Files
pws2mqtt-qt/pws2mqtt.cpp
2025-10-13 22:24:44 +02:00

701 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "pws2mqtt.h"
#include "mqtt.h"
//#include "httpserver.h"
#include <unistd.h>
#include <QtGlobal>
#include <iostream>
#include <QList>
#include <sys/socket.h>
#include <netinet/in.h>
#include <QtHttpServer/QHttpServer>
#include <QtHttpServer/QHttpServerRequest>
#include <curl/curl.h>
#include <QtMath>
#include <QImage>
#include <QTransform>
#include <stdio.h>
#include <sys/stat.h>
extern MqttClient *mqttClient;
extern Pws2mqtt *pws2mqtt;
extern QHttpServer *httpServer;
QStringList previsionList
{
"Temps variable (incertain).", \
"Beau temps stable (anticyclone).", \
"Beau temps en baisse (anticyclone).", \
"Amélioration progressive (éclaircies).", \
"Risque de pluie légère ou nuages.", \
"Dégradation marquée (pluie/vent/orage).", \
"Tempête ou dépression forte (vigilance).", \
"Temps stable"
};
// Callback for curl library
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
(void)contents; // Évite les warnings de compilation
(void)userp;
return size * nmemb; // On ne traite pas les données reçues
}
QMap <QByteArray, QString> propertyList;
QMap <QByteArray, double> propertiesValue;
QMap <QByteArray, QPair<QString, QByteArray>> propertyName
{
{"tempf", {"Température extérieure", "°C"}},
{"humidity", {"Humidité extérieure", "%"}},
{"dewptf", {"Point de rosée", "°C"}},
{"windchillf", {"Température ressentie", "°C"}},
{"winddir", {"Direction du vent", "°"}},
{"windspeedmph", {"vitesse du vent", " km/h"}},
{"windgustmph", {"Rafales", " km/h"}},
{"rainin", {"Pluie", " mm/h"}},
{"dailyrainin", {"Pluie de la journée", " mm"}},
{"monthlyrainin", {"Pluie du mois", " mm"}},
{"yearlyrainin", {"Pluie de l'année", " mm"}},
{"solarradiation", {"Énergie solaire", " W/m²"}},
{"indoortempf", {"Température intérieure", "°C"}},
{"indoorhumidity", {"Humidité intérieure", "%"}},
{"baromin", {"pression atmosphérique", " hPa"}},
{"lowbatt", {"Alerte batterie faible", ""}},
{"UV", {"Alerte UV", ""}}
};
QList <QList <QVariant>> forceVent
{
// { max wind speed, name, priority}
{1, "Temps calme", 1},
{5, "Très légère brise", 1},
{11, "Légère brise", 1},
{19, "Petite brise", 1},
{28, "Jolie brise", 1},
{38, "Bonne brise", 1},
{49, "Vent frais", 3},
{61, "Grand vent", 4},
{74, "Vent fort", 5},
{88, "Vent violent", 5},
{117, "Tempête", 5},
{1000, "Ouragan", 5}
};
Pws2mqtt::Pws2mqtt()
{
this->init();
}
Pws2mqtt::~Pws2mqtt()
{
}
void Pws2mqtt::init()
{
debug(DEBUGMACRO, "init http server", DEBUG);
httpServer->setMissingHandler([](const QHttpServerRequest &request, QHttpServerResponder &&responder)
{
(void) responder;
debug(DEBUGMACRO, "body " + request.url().toString(), WARNING);
return QHttpServerResponse("404 - Page non trouvée. Route par défaut.", QHttpServerResponse::StatusCode::NotFound);
//responder.write(QHttpServerResponse("404 - Page non trouvée. Route par défaut.", QHttpServerResponse::StatusCode::NotFound));
});
httpServer->route("/", [](const QHttpServerRequest &request)
{
QList<std::pair<QByteArray, QByteArray>> headersList = request.headers().toList();
debug(DEBUGMACRO, "Remote address" + request.remoteAddress().toString(), WARNING);
return QHttpServerResponse("text/plain", "Failed\n");
});
httpServer->route("/query", [this](const QHttpServerRequest &request)
{
QByteArray data;
QList<std::pair<QString, QString>> queryList = request.query().queryItems();
//QTextStream result(&data);
if (queryList.isEmpty())
{
debug(DEBUGMACRO, "Request query is empty", WARNING);
}else
{
debug(DEBUGMACRO, "Request query :" + request.query().toString() , DEBUG);
this->parseData(queryList);
debug(DEBUGMACRO, "Returning 'success'", DEBUG);
}
//mqttClient.send_message(jsonString);
return QHttpServerResponse("text/plain", "Success\n");
});
}
void Pws2mqtt::listeningHttp()
{
//QByteArray data;
debug (DEBUGMACRO, "listening http requests", DEBUG);
const auto port = httpServer->listen(QHostAddress::Any, 5000);
if (!port)
{
debug(DEBUGMACRO, "Http Server failed to listen on a port.", ERROR);
}
debug(DEBUGMACRO, "Listening on port " + QString::number(port));
}
void Pws2mqtt::parseData(QList<std::pair<QString, QString>> queryList)
{
debug(DEBUGMACRO, "Parsing Datas", DEBUG);
QString jsonString = "{";
bool propertyFlag = false;
bool deviceFlag = false;
// QString topic;
QString deviceString = "\"device\": {\"ieeeAddress\": \"" + mqttClient->macAddress + "\", \"type\": \"" + mqttClient->type + "\", \"powerSource\": \"Battery\"";
QString notif = "";
double propertyValue = 0;
QStringList priorityList {"", "min", "low", "default", "High", "urgent"};
quint8 priority = 2;
//static qreal winddir = 0;
QString attachment = "";
static QDateTime timer;
debug(DEBUGMACRO, "looping list of query", DEBUG);
for (QPair<QString, QString> pair : queryList)
{
debug(DEBUGMACRO, pair.first + " = " + pair.second, DEBUG);
QByteArray name = pair.first.toLatin1();
QByteArray value = pair.second.toLatin1();
float fValue = value.toDouble();
if (this->deviceProperties.contains(name))
{
if(deviceFlag == false)
{
deviceFlag = true;
}else
{
deviceString.append(", ");
}
deviceString += "\"" + name + "\": ";
deviceString += pair.second;
}else
{
if(propertyFlag == false)
{
propertyFlag = true;
}else
{
jsonString.append(", ");
}
jsonString.append("\"" + name + "\": ");
jsonString.append(pair.second);
if (name == "tempf")
{
static QDateTime timeTemp = QDateTime::currentDateTime().addSecs(-600);
propertyValue = round(fahrenheitToCelsius(fValue));
debug (DEBUGMACRO, "timeTemp = " + timeTemp.time().toString() + " - current :" + QDateTime::currentDateTime().toString() , DEBUG);
if (compare (propertiesValue[name], propertyValue, 1) and timeTemp < QDateTime::currentDateTime())
{
//notif += formatNotifString (propertyName[name].first, propertyName[name].second, QByteArray::number(qPow(propertyValue, 1.0)));
//debug (DEBUGMACRO, "", DEBUG);
timeTemp = timeTemp.currentDateTime().addSecs(300);
propertyList[name] = formatNotifString(propertyName[name].first, propertyName[name].second, QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
}
}else if (name == "humidity")
{
propertyValue = value.toFloat();
debug (DEBUGMACRO, name + " : " + QByteArray::number(propertyValue), DEBUG);
if (compare (propertiesValue[name], propertyValue, 3))
{
//notif += formatNotifString (propertyName[name].first, propertyName[name].second , value);
propertyList[name] = formatNotifString(propertyName[name].first, propertyName[name].second, QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
}
}else if (name == "windchillf")
{
static QDateTime timeWindchill = QDateTime::currentDateTime().addSecs(-600);
propertyValue = round(fahrenheitToCelsius(value.toFloat()));
debug (DEBUGMACRO, name + " : " + QByteArray::number(propertyValue), DEBUG);
if (compare (propertiesValue[name], propertyValue, 1) and timeWindchill < QDateTime::currentDateTime())
{
//notif += formatNotifString (propertyName[name].first, propertyName[name].second , QByteArray::number(qPow(propertyValue, 1.0)));
//debug (DEBUGMACRO, "Notif = #" + notif + "#", DEBUG);
timeWindchill = timeWindchill.currentDateTime().addSecs(300);
propertyList[name] = formatNotifString(propertyName[name].first, propertyName[name].second, QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
}
}else if (name == "winddir")
{
propertyValue = value.toFloat();
rotateAndSaveImage(this->inputPath, this->outputPath, (qreal)propertyValue);
debug (DEBUGMACRO, name + " : " + QByteArray::number(propertyValue), DEBUG);
propertyList[name] = formatNotifString(propertyName[name].first, propertyName[name].second, QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
}else if (name == "windspeedmph" or name == "windgustmph")
{
QString msg = "";
static QDateTime timeWind = QDateTime::currentDateTime().addSecs(-600);
quint8 windPriority = 1;
//static QString msgNotif;
debug (DEBUGMACRO, name + " : " + QByteArray::number(propertyValue), DEBUG);
propertyValue = round(mphTokmh(value.toFloat()));
for (quint8 i=0; i<forceVent.size();i++)
{
if (propertyValue <= forceVent[i][0].toDouble())
{
msg = forceVent[i][1].toString();
windPriority = forceVent[i][2].toUInt();
break;
}
}
if (propertyList["forcevent"] != msg or propertyList["rafales"] != msg)
{
if (name == "windspeedmph")
{
//propertyList["forcevent"] = msg;
msg.prepend("Vent - ");
propertyList[name] = formatNotifString (msg, propertyName[name].second , QByteArray::number(propertyValue));
}else
{
propertyList[name] = formatNotifString (propertyName[name].first, propertyName[name].second , QByteArray::number(propertyValue));
}
setPriority(priority, windPriority);
attachment = this->outputPath;
}
}else if (name == "rainin")
{
static double ecart;
quint8 raininPriority = 1;
propertyValue = round(pair.second.toFloat()*100)/100;
if (propertyValue == 0)
{
ecart = 0.0001;
}else
{
ecart = 0.01;
}
debug (DEBUGMACRO, name + " : " + QByteArray::number(propertyValue), DEBUG);
if (compare (propertiesValue[name], propertyValue, ecart))
{
QString pluviosite = getPluviosite(propertyValue, raininPriority);
priority = setPriority (priority, raininPriority);
//debug (DEBUGMACRO, "Notif = #" + notif + "#", DEBUG);
propertyList[name] = formatNotifString(pluviosite + " : ", propertyName[name].second, QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
}
}else if (name == "dailyrainin")
{
static double ecart;
propertyValue = round(pair.second.toFloat()*100)/100;
if (propertyValue == 0)
{
ecart = 0.0001;
}else
{
ecart = 0.01;
}
debug (DEBUGMACRO, name + " : " + QByteArray::number(propertyValue), DEBUG);
if (compare (propertiesValue[name], propertyValue, ecart))
{
priority = setPriority (priority, 3);
//debug (DEBUGMACRO, "Notif = #" + notif + "#", DEBUG);
propertyList[name] = formatNotifString(propertyName[name].first, propertyName[name].second, QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
}
}else if (name == "baromin")
{
//static QDateTime timePrevision = QDateTime::currentDateTime().addSecs(-1000);
propertyValue = tohPa(pair.second.toFloat());
debug (DEBUGMACRO, "Barometre en hPa : " + QByteArray::number(propertyValue), DEBUG);
if (compare (propertiesValue[name], propertyValue, 0.5))
{
//notif += formatNotifString (propertyName[name].first, propertyName[name].second , QByteArray::number(propertyValue));
//debug (DEBUGMACRO, "Notif = #" + notif + "#", DEBUG);
propertyList[name] = formatNotifString(propertyName[name].first, "", QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
}
static QString prevision;
quint8 prevPriority = 0;
QString ret = pressureVariation(propertyValue, prevPriority);
if (!ret.isEmpty())
{
debug (DEBUGMACRO, "baromin ret not empty : " + ret, DEBUG);
QString newPrevision = ret;
priority = setPriority(priority, prevPriority);
if (prevision != newPrevision)
{
prevision = newPrevision;
propertyList["prevision"] = "- " + prevision + " \n";
}
}
}else if (name == "UV")
{
static QDateTime timeUV = QDateTime::currentDateTime().addSecs(-600);
propertyValue = pair.second.toFloat();
debug (DEBUGMACRO, name + " : " + QByteArray::number(propertyValue), DEBUG);
if (compare (propertiesValue[name], propertyValue, 1) and timeUV < QDateTime::currentDateTime())
{
//notif += formatNotifString (propertyName[name].first, propertyName[name].second , value);
if (propertyValue == 5 )
priority = setPriority (priority, 4);
if (propertyValue >= 6 )
priority = setPriority (priority, 5);
timeUV = timeUV.currentDateTime().addSecs(300);
propertyList[name] = formatNotifString(propertyName[name].first, "", QByteArray::number(propertyValue));
propertiesValue[name] = propertyValue;
debug (DEBUGMACRO, "Notif = #" + notif + "#", DEBUG);
}
}
}
}
if (!jsonString.isEmpty())
{
debug(DEBUGMACRO, "json string : " + jsonString, DEBUG);
jsonString = jsonString +", " + deviceString + "}}";
mqttClient->send_message(jsonString);
}else
{
debug(DEBUGMACRO, "No values to send", DEBUG);
}
debug(DEBUGMACRO, "current datetime : " + QDateTime::currentDateTime().toString() + ", timer = " + timer.toString(), DEBUG);
if (priority > 3 or QDateTime::currentDateTime() > timer.addSecs(900))
{
timer = QDateTime::currentDateTime();
debug(DEBUGMACRO, "looping to fill notif, priority = " + QString::number(priority), DEBUG);
for (auto [name, value]: propertyList.asKeyValueRange())
{
debug(DEBUGMACRO, "Name = " + name + ", value = " + value, DEBUG);
notif += propertyList[name];
}
debug(DEBUGMACRO, "calling notify with notif = #" + notif + "#", DEBUG);
notify (notif, priorityList[priority], attachment);
}
debug(DEBUGMACRO, "parseData: Returning", DEBUG);
}
QString getPluviosite(double value, quint8 &priority)
{
if (value < 1)
{
return "Pluie fine, bruine";
}else if (value < 5)
{
return "Pluie Modérée";
}else if (value < 10)
{
priority = 4;
return "Pluie forte";
}else if (value < 30)
{
priority = 5;
return "Pluie Très forte, risque d'inondation";
}else
{
priority = 5;
return "Pluie diluvienne, risque élevé d'inondations, crues";
}
}
QString previsionMeteo(double currentPressure, double variation3h, quint8 &priority)
{
QString variation = "";
priority = 3;
debug (DEBUGMACRO, "begin", DEBUG);
if (variation3h == 0.0)
{
variation = " - temps stable";
}else if (variation3h > 0.2)
{
variation = " en amélioration rapide";
}else if (variation3h < -2.0)
{
variation = "en dégradation rapide";
}else if (variation3h > 0)
{
variation = " en amélioration";
}else if (variation3h <0)
{
variation = " en dégradation";
}
if (currentPressure > 1020.0)
{
return "Beau temps (anticyclone)" + variation;
} else if (currentPressure > 1010.0 && currentPressure <= 1020.0)
{
return "Éclaircies" + variation;
} else if (currentPressure > 1000.0 && currentPressure <= 1010.0)
{
priority = 4;
return "Temps variable, risque de pluie légère" + variation;
} else if (currentPressure >=990 && currentPressure <= 1000.0)
{
priority = 5;
return "Pluie/vent/orage" + variation;
} else if (currentPressure < 990.0)
{
priority = 5;
return "Tempête ou dépression forte (vigilance)" + variation;
}
return variation;
}
QString pressureVariation(double currentPressure, quint8 &priority)
{
const int NB_MESURES_3H = 360; // 3h × 120 mesures/heure (1 mesure toutes les 30s)
static QVector<double> historiquePressions;
debug(DEBUGMACRO, "PressureVariation : current pressure = " + QByteArray::number(currentPressure), DEBUG);
historiquePressions.append(currentPressure);
// Si on a assez de mesures pour couvrir 3h
if (historiquePressions.size() > NB_MESURES_3H)
{
debug(DEBUGMACRO, "historiquePressions > " + QByteArray::number(NB_MESURES_3H), DEBUG);
historiquePressions.erase(historiquePressions.begin());
}
debug(DEBUGMACRO, "taille de historiquePression : " + QByteArray::number(historiquePressions.size()), DEBUG);
// Calcul de la variation sur 3h si on a assez de mesures
if (historiquePressions.size() == NB_MESURES_3H)
{
double pressionInitiale = historiquePressions.front();
double pressionFinale = historiquePressions.back();
double variation = pressionFinale - pressionInitiale;
debug(DEBUGMACRO, "Pression actuelle : " + QString::number(pressionFinale) + " hPa", DEBUG);
debug(DEBUGMACRO, "Variation sur 3h : " + QString::number(variation) + " hPa", DEBUG);
// Prévision météo
QString prevision = previsionMeteo(pressionFinale, variation, priority);
debug(DEBUGMACRO, "Prévision : " + prevision, DEBUG);
// Exemple : Envoi d'une alerte si nécessaire
debug(DEBUGMACRO, "ending pressureVariation with result", DEBUG);
return prevision;
}else
{
static QTime time = QTime::currentTime().addSecs(NB_MESURES_3H * 30);
return "Première prévision à " + time.toString();
}
debug(DEBUGMACRO, "ending pressureVariation no result", DEBUG);
return "";
}
quint8 setPriority (quint8 currentPriority, quint8 newPriority)
{
if (newPriority > currentPriority)
{
return newPriority;
}else
{
return currentPriority;
}
}
double fahrenheitToCelsius (double fahrenheit)
{
return (fahrenheit - 32.0) * 5.0 / 9.0;
}
double tohPa (double value)
{
return round(value * 33.8639 * 100) / 100;
}
double mphTokmh (double value)
{
return value * 1.60934;
}
bool compare (double value, double currentValue, double ecart)
{
debug(DEBUGMACRO, "value: " + QByteArray::number(value) + "testValue: " + QByteArray::number(currentValue), DEBUG);
if (currentValue <= (value - ecart) or currentValue >= (value + ecart))
{
debug(DEBUGMACRO, "compare return true", DEBUG);
return true;
}else
{
debug(DEBUGMACRO, "compare return false", DEBUG);
return false;
}
}
QString formatNotifString (QString name, QString unit ,QByteArray value)
{
QString text = "- " + name + " : " + value;
if (!unit.isEmpty())
{
text += unit;
//"- " + propertyName[name].first + " : " + value + " " + propertyName[name].second + " \n";
}
return text += " \n";
}
static size_t ReadFile(void *ptr, size_t size, size_t nmemb, void *stream) {
FILE *f = (FILE *)stream;
size_t n = fread(ptr, size, nmemb, f);
return n;
}
void notify(QString notif, QString priority, QString inputPath)
{
CURL *curl;
CURLcode res;
std::string readBuffer;
priority = "Priority: " + priority;
debug (DEBUGMACRO, "notifying at priority " + priority + " - message : " + notif, DEBUG);
// Initialise libcurl
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if (curl)
{
// Définis les en-têtes pour le titre et les priorités
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Title: Météo");
headers = curl_slist_append(headers, priority.toStdString().c_str());
headers = curl_slist_append(headers, "Markdown: yes");
headers = curl_slist_append(headers, "Config: /etc/ntfy.client.yml");
headers = curl_slist_append(headers, "Firebase: no");
/*curl_mime *mime;
curl_mimepart *part;
// Initialise la structure mime
mime = curl_mime_init(curl);
// Ajoute le champ "message" (ton texte de notification)
part = curl_mime_addpart(mime);
curl_mime_name(part, "message");
curl_mime_data(part, notif.toUtf8().constData(), CURL_ZERO_TERMINATED);
*/
// Configure la requête POST
curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:81/Meteo");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, notif.toUtf8().constData());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
// Exécute la requête
res = curl_easy_perform(curl);
// Vérifie les erreurs
if (res != CURLE_OK)
{
debug(DEBUGMACRO, "Erreur libcurl :" + QString(curl_easy_strerror(res)), DEBUG);
} else
{
debug(DEBUGMACRO, "Notification envoyée avec succès", DEBUG);
}
// Nettoie les ressources
//curl_mime_free(mime);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
// Nettoie libcurl
curl_global_cleanup();
if (!inputPath.isEmpty())
{
FILE *fd = fopen(inputPath.toStdString().c_str(), "rb");
if (!fd) {
perror("Erreur lors de l'ouverture du fichier");
return;
}
// Initialise libcurl
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if (curl)
{
// Définis les en-têtes pour le titre et les priorités
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Title: Météo");
headers = curl_slist_append(headers, priority.toStdString().c_str());
headers = curl_slist_append(headers, "Config: /etc/ntfy.client.yml");
headers = curl_slist_append(headers, "Firebase: no");
headers = curl_slist_append(headers, "Content-Type: image/png");
headers = curl_slist_append(headers, "X-Original-Size: true");
headers = curl_slist_append(headers, "X-Filename: fleche.png");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // Configurer l'URL
curl_easy_setopt(curl, CURLOPT_URL, "localhost:81/Meteo");
// Utiliser la méthode PUT (comme `curl -T`)
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
// Lire le fichier et l'envoyer en tant que body
curl_easy_setopt(curl, CURLOPT_READFUNCTION, ReadFile);
curl_easy_setopt(curl, CURLOPT_READDATA, fd);
// Définir la taille du fichier (optionnel mais recommandé)
struct stat file_info;
fstat(fileno(fd), &file_info);
curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)file_info.st_size);
// Exécuter la requête
res = curl_easy_perform(curl);
// Vérifie les erreurs
if (res != CURLE_OK)
{
debug(DEBUGMACRO, "Erreur libcurl :" + QString(curl_easy_strerror(res)), DEBUG);
} else
{
debug(DEBUGMACRO, "Notification envoyée avec succès", DEBUG);
}
// Nettoyer
fclose(fd);
curl_easy_cleanup(curl);
}
// Nettoie libcurl
curl_global_cleanup();
}
}
void rotateAndSaveImage(const QString &inputPath, const QString &outputPath, qreal angle)
{
QImage image(inputPath);
if (image.isNull())
{
debug (DEBUGMACRO, "Impossible de charger l'image : " + inputPath, WARNING);
return;
}
// Crée une transformation pour pivoter l'image
QTransform transform;
// Décale le centre de l'image à l'origine
transform.translate(-image.width() / 2.0, -image.height() / 2.0);
// Applique la rotation
transform.rotate(angle);
// Remet le centre à sa position d'origine
transform.translate(image.width() / 2.0, image.height() / 2.0);
// Applique la transformation
QImage rotatedImage = image.transformed(transform, Qt::SmoothTransformation);
// Enregistre l'image pivotée
if (!rotatedImage.save(outputPath))
{
debug (DEBUGMACRO, "Impossible d'enregistrer l'image : ", WARNING);
}
}