From 9a594e05cc2b69802497f7b82ec580c2f09053b8 Mon Sep 17 00:00:00 2001 From: "de@itstall.de" <de@itstall.de> Date: Mon, 3 Feb 2020 04:02:27 +0100 Subject: [PATCH] closes #3 closes #2 --- MqttClient.h | 9 +- backend.cpp | 83 ++++++++++++------ dbSqlite.h | 209 ++++++++++++++++++++++++++++++---------------- influxdb.h | 1 + openweathermap.h | 34 +++++--- structs.h | 24 ++++-- weatherstation.db | Bin 552960 -> 552960 bytes 7 files changed, 238 insertions(+), 122 deletions(-) diff --git a/MqttClient.h b/MqttClient.h index bda20d2..0708316 100644 --- a/MqttClient.h +++ b/MqttClient.h @@ -73,7 +73,10 @@ public: // * qos (0,1,2) // * retain (boolean) - indicates if message is retained on broker or not // Should return MOSQ_ERR_SUCCESS - int ret = publish(NULL, topic, sizeof(message), message, db->getSettings().mqtt_qos, true); + int ret = publish(NULL, topic, strlen(message), message, db->getSettings().mqtt_qos, true); + if (DEBUG) std::cout << "Published: " << std::endl; + if (DEBUG) std::cout << "Topic: " << topic << std::endl; + if (DEBUG) std::cout << "Message: " << message << std::endl; return (ret == MOSQ_ERR_SUCCESS); } @@ -94,14 +97,14 @@ public: std::string str(buf); - if (str.find("Request_") != std::string::npos) { + /*if (str.find("Request_") != std::string::npos) { std::cout << "MqttClient::on_message() - Request found" << std::endl; std::string client = str.substr(str.find("_") + 1); std::cout << "Client " + client + " requested update" << std::endl; std::string result = db->getFrontendData().c_str(); snprintf(buf, payload_size, result.c_str()); publish(NULL, db->getSettings().mqtt_topic_frontend.c_str(), strlen(result.c_str()), result.c_str()); - } + }*/ } } }; \ No newline at end of file diff --git a/backend.cpp b/backend.cpp index 8963d22..b1f07ee 100644 --- a/backend.cpp +++ b/backend.cpp @@ -1,8 +1,9 @@ #include <iostream> #include <thread> -#include <vector> #include <chrono> #include <ctime> +#include <vector> +#include <string> #include "openweathermap.h" #include "MqttClient.h" #include "dbSqlite.h" @@ -10,6 +11,11 @@ #define DEBUG true +using std::string; +using std::cout; +using std::endl; +using std::thread; + // Database object dbSqlite* db; openweathermap* owmw; @@ -17,45 +23,70 @@ MqttClient* mqttClient; // Thread for openweathermap:getWeather void schedulerWeather(int time) { - if (DEBUG) std::cout << "Wetterstation::schedulerWeather()" << std::endl; + if (DEBUG) cout << "Wetterstation::schedulerWeather()" << endl; + string topic; + string weather; + std::vector<Region> region; + std::vector<Frontend> frontend; + while (1) { - owmw->getWeather(db->getSettings().owm_plz, db->getSettings().owm_lngCode); + region = db->queryRegions(); + frontend = db->queryFrontends(); + for (int i = 0; i < region.size(); i++) { + if (DEBUG) cout << "Wetterstation::schedulerWeather(" + region[i].plz + ")" << endl; + owmw->getWeather(region[i].plz, region[i].lngCode); + weather = db->getFrontendDataWeather().c_str(); + for (int fr = 0; fr < frontend.size(); fr++) { + if (frontend[fr].plz == region[i].plz) { + topic = db->getSettings().mqtt_topic_frontend + frontend[fr].frontendId + "/weather"; + mqttClient->send_message(weather.c_str(), topic.c_str()); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } std::this_thread::sleep_for(std::chrono::milliseconds(time)); } } // Thread for openweathermap:getForecast void schedulerForecast(int time) { - if (DEBUG) std::cout << "Wetterstation::schedulerForecast()" << std::endl; + if (DEBUG) cout << "Wetterstation::schedulerForecast()" << endl; + string topic; + string forecast; + std::vector<Region> region; + std::vector<Frontend> frontend; + while (1) { - owmw->getForecast(db->getSettings().owm_plz, db->getSettings().owm_lngCode); + region = db->queryRegions(); + frontend = db->queryFrontends(); + for (int i = 0; i < region.size(); i++) { + if (DEBUG) cout << "Wetterstation::schedulerForecast(" + region[i].plz + ")" << endl; + owmw->getForecast(region[i].plz, region[i].lngCode); + forecast = db->getFrontendDataForecast().c_str(); + for (int fr = 0; fr < frontend.size(); fr++) { + if (frontend[fr].plz == region[i].plz) { + topic = db->getSettings().mqtt_topic_frontend + frontend[i].frontendId + "/forecast"; + mqttClient->send_message(forecast.c_str(), topic.c_str()); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } std::this_thread::sleep_for(std::chrono::milliseconds(time)); } } -// Thread for MQTT:sendWeather -//void schedulerMqttSendWeather(int time) { -// if (DEBUG) std::cout << "Wetterstation::schedulerMqttSendWeather()" << std::endl; -// while (1) { -// //mqttClient->publish(); -// std::this_thread::sleep_for(std::chrono::milliseconds(time)); -// } -//} - void mqttStart() { - if (DEBUG) std::cout << "Wetterstation::mqttStart()" << std::endl; - + if (DEBUG) cout << "Wetterstation::mqttStart()" << endl; mosqpp::lib_init(); - mqttClient = new MqttClient("Wetterstation", db); - //std::thread thrMqttSendWeather{ schedulerMqttSendWeather, 10000 }; - mosqpp::lib_cleanup(); } // Main method int main() { - if (DEBUG) std::cout << "Wetterstation::main()" << std::endl; + if (DEBUG) cout << "Wetterstation::main()" << endl; // Sockets aktivieren WSADATA wsaData; @@ -70,19 +101,17 @@ int main() { owmw = new openweathermap(db); + // MQTT starten + mqttStart(); + // Thread für getWeather starten - std::thread thrWeather{ schedulerWeather, 60000 }; + thread thrWeather{ schedulerWeather, 60000 }; std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Thread für getForecast starten - std::thread thrForecast{ schedulerForecast, 90000 }; + thread thrForecast{ schedulerForecast, 90000 }; std::this_thread::sleep_for(std::chrono::milliseconds(50)); - // MQTT starten - mqttStart(); - - std::vector<WeatherConditions> cond = db->getConditions(); - while (1) { Sleep(1000); } diff --git a/dbSqlite.h b/dbSqlite.h index 6bb7667..ff71ca5 100644 --- a/dbSqlite.h +++ b/dbSqlite.h @@ -2,10 +2,10 @@ #include <iostream> #include <string> #include <vector> -#include "influxdb.h" #include "Poco/Data/Session.h" #include "Poco/Data/SQLite/Connector.h" #include "nlohmann/json.hpp" +#include "influxdb.h" #include "structs.h" using namespace Poco::Data::Keywords; @@ -13,122 +13,142 @@ using Poco::Data::Session; using Poco::Data::Statement; using std::string; using std::cout; +using std::endl; -#define DEBUG false +#define DEBUG true class dbSqlite { private: - std::vector<WeatherConditions> weatherConditions; Settings settings; - std::vector<weatherData> vecForecast; - weatherData sWeather; + std::vector<WeatherData> vecForecast; + WeatherData sWeather; public: string dbFile; dbSqlite() { - if (DEBUG) std::cout << "sbSqlite()" << std::endl; + if (DEBUG) cout << "sbSqlite()" << endl; Poco::Data::SQLite::Connector::registerConnector(); this->dbFile = "weatherstation.db"; - this->weatherConditions = this->queryConditions(); this->querySettings(); }; Settings getSettings() { - if (DEBUG) std::cout << "dbSqlite::getSettings()" << std::endl; + if (DEBUG) cout << "dbSqlite::getSettings()" << endl; return this->settings; } - std::vector<WeatherConditions> getConditions() { - return this->weatherConditions; + WeatherData getSWeather() { + return this->sWeather; } - weatherData getSWeather() { - return this->sWeather; + string getFrontendDataWeather() { + if (DEBUG) cout << "dbSqlite::getFrontendDataWeather()" << endl; + nlohmann::json jObj; + + jObj["plz"] = sWeather.plz; + jObj["lngCode"] = sWeather.lngCode; + jObj["sunrise"] = sWeather.sunrise; + jObj["sunset"] = sWeather.sunset; + jObj["visibility"] = sWeather.visibility; + jObj["temp"] = sWeather.temp; + jObj["tempFeelsLike"] = sWeather.tempFeelsLike; + jObj["tempMin"] = sWeather.tempMin; + jObj["tempMax"] = sWeather.tempMax; + jObj["humidity"] = sWeather.humidity; + jObj["pressure"] = sWeather.pressure; + jObj["windSpeed"] = sWeather.windSpeed; + jObj["windDeg"] = sWeather.windDeg; + jObj["clouds"] = sWeather.clouds; + jObj["rain1h"] = sWeather.rain1h; + jObj["rain3h"] = sWeather.rain3h; + jObj["snow1h"] = sWeather.snow1h; + jObj["snow3h"] = sWeather.snow3h; + jObj["icon1Id"] = sWeather.icon1Id; + jObj["icon1"] = sWeather.icon1; + jObj["icon2Id"] = sWeather.icon2Id; + jObj["icon2"] = sWeather.icon2; + + return jObj.dump(); } - std::string getFrontendData() { + string getFrontendDataForecast() { + if (DEBUG) cout << "dbSqlite::getFrontendDataForecast()" << endl; nlohmann::json jObj; + int index = 0; + + for (int i = 0; i < vecForecast.size(); i++) { + if (vecForecast[i].from.find("12:00:00") != string::npos) { + jObj[index]["plz"] = vecForecast[i].plz; + jObj[index]["lngCode"] = vecForecast[i].lngCode; + jObj[index]["sunrise"] = vecForecast[i].sunrise; + jObj[index]["sunset"] = vecForecast[i].sunset; + jObj[index]["visibility"] = vecForecast[i].visibility; + jObj[index]["temp"] = vecForecast[i].temp; + jObj[index]["tempFeelsLike"] = vecForecast[i].tempFeelsLike; + jObj[index]["tempMin"] = vecForecast[i].tempMin; + jObj[index]["tempMax"] = vecForecast[i].tempMax; + jObj[index]["humidity"] = vecForecast[i].humidity; + jObj[index]["pressure"] = vecForecast[i].pressure; + jObj[index]["windSpeed"] = vecForecast[i].windSpeed; + jObj[index]["windDeg"] = vecForecast[i].windDeg; + jObj[index]["clouds"] = vecForecast[i].clouds; + jObj[index]["rain1h"] = vecForecast[i].rain1h; + jObj[index]["rain3h"] = vecForecast[i].rain3h; + jObj[index]["snow1h"] = vecForecast[i].snow1h; + jObj[index]["snow3h"] = vecForecast[i].snow3h; + jObj[index]["icon1Id"] = vecForecast[i].icon1Id; + jObj[index]["icon1"] = vecForecast[i].icon1; + jObj[index]["icon2Id"] = vecForecast[i].icon2Id; + jObj[index]["icon2"] = vecForecast[i].icon2; + jObj[index]["from"] = vecForecast[i].from; + index++; + } + } + + return jObj.dump(); + } - std::string queryResult; + string getFrontendDataSensors() { + if (DEBUG) cout << "dbSqlite::getFrontendDataSensors()" << endl; + nlohmann::json jObj; + int i = 0; + int j = 0; + + string queryResult; influxdb_cpp::server_info si(this->getSettings().influx_host, this->getSettings().influx_port, this->getSettings().influx_db, this->getSettings().influx_user, this->getSettings().influx_pass); influxdb_cpp::query(queryResult, "SELECT * FROM Sensors WHERE client = 'DVES_06236C' AND time > now() - 5m;", si); auto response = nlohmann::json::parse(queryResult); nlohmann::json columns = response["results"][0]["series"][0]["columns"]; - - int i = 0; - int j = 0; for (nlohmann::json line : response["results"][0]["series"][0]["values"]) { j = 0; nlohmann::json temp; for (nlohmann::json column : columns) { - temp[column.get<std::string>()] = line[j]; + temp[column.get<string>()] = line[j]; j++; } - jObj["sensors"][i++] = temp; + jObj[i++] = temp; } - for (int i = 0; i < vecForecast.size(); i++) { - jObj["forecast"][i]["plz"] = vecForecast[i].plz; - jObj["forecast"][i]["lngCode"] = vecForecast[i].lngCode; - jObj["forecast"][i]["sunrise"] = vecForecast[i].sunrise; - jObj["forecast"][i]["sunset"] = vecForecast[i].sunset; - jObj["forecast"][i]["visibility"] = vecForecast[i].visibility; - jObj["forecast"][i]["temp"] = vecForecast[i].temp; - jObj["forecast"][i]["tempFeelsLike"] = vecForecast[i].tempFeelsLike; - jObj["forecast"][i]["tempMin"] = vecForecast[i].tempMin; - jObj["forecast"][i]["tempMax"] = vecForecast[i].tempMax; - jObj["forecast"][i]["humidity"] = vecForecast[i].humidity; - jObj["forecast"][i]["pressure"] = vecForecast[i].pressure; - jObj["forecast"][i]["windSpeed"] = vecForecast[i].windSpeed; - jObj["forecast"][i]["windDeg"] = vecForecast[i].windDeg; - jObj["forecast"][i]["clouds"] = vecForecast[i].clouds; - jObj["forecast"][i]["rain1h"] = vecForecast[i].rain1h; - jObj["forecast"][i]["rain3h"] = vecForecast[i].rain3h; - jObj["forecast"][i]["snow1h"] = vecForecast[i].snow1h; - jObj["forecast"][i]["snow3h"] = vecForecast[i].snow3h; - jObj["forecast"][i]["icons"] = vecForecast[i].icons; - jObj["forecast"][i]["from"] = vecForecast[i].from; - } - jObj["current"]["plz"] = sWeather.plz; - jObj["current"]["lngCode"] = sWeather.lngCode; - jObj["current"]["sunrise"] = sWeather.sunrise; - jObj["current"]["sunset"] = sWeather.sunset; - jObj["current"]["visibility"] = sWeather.visibility; - jObj["current"]["temp"] = sWeather.temp; - jObj["current"]["tempFeelsLike"] = sWeather.tempFeelsLike; - jObj["current"]["tempMin"] = sWeather.tempMin; - jObj["current"]["tempMax"] = sWeather.tempMax; - jObj["current"]["humidity"] = sWeather.humidity; - jObj["current"]["pressure"] = sWeather.pressure; - jObj["current"]["windSpeed"] = sWeather.windSpeed; - jObj["current"]["windDeg"] = sWeather.windDeg; - jObj["current"]["clouds"] = sWeather.clouds; - jObj["current"]["rain1h"] = sWeather.rain1h; - jObj["current"]["rain3h"] = sWeather.rain3h; - jObj["current"]["snow1h"] = sWeather.snow1h; - jObj["current"]["snow3h"] = sWeather.snow3h; - jObj["current"]["icons"] = sWeather.icons; - return jObj.dump(); } - std::string getFrontendJson() { + string getFrontendJson() { nlohmann::json combinedjObj; } - void setSWeather(weatherData wd) { + void setSWeather(WeatherData wd) { this->sWeather = wd; } - void setSForecast(std::vector<weatherData> fc) { + void setSForecast(std::vector<WeatherData> fc) { this->vecForecast = fc; } void querySettings() { - if (DEBUG) std::cout << "dbSqlite::querySettings()" << std::endl; + if (DEBUG) cout << "dbSqlite::querySettings()" << endl; Session session("SQLite", this->dbFile); Statement select(session); @@ -153,22 +173,63 @@ public: select.execute(); } - std::vector<WeatherConditions> queryConditions() { - if (DEBUG) std::cout << "dbSqlite::queryConditions()" << std::endl; + std::vector<Region> queryRegions() { + if (DEBUG) cout << "dbSqlite::queryRegions()" << endl; + Session session("SQLite", this->dbFile); + std::vector<Region> result; + Region region; + + Statement select(session); + select << "SELECT plz, lngCode FROM settings_frontend GROUP BY plz;", + into(region.plz), + into(region.lngCode), + range(0, 1); // iterate over result set one row at a time + + while (!select.done()) { + select.execute(); + result.push_back(region); + } + + return result; + } + + std::vector<Frontend> queryFrontendsByPlz(string plz) { + if (DEBUG) cout << "dbSqlite::queryFrontendsByPlz(" << plz << ")" << endl; + Session session("SQLite", this->dbFile); + std::vector<Frontend> result; + Frontend frontend; + + Statement select(session); + select << "SELECT frontendId, lngCode, plz FROM settings_frontend WHERE plz = \"" + plz + "\";", + into(frontend.frontendId), + into(frontend.lngCode), + into(frontend.plz), + range(0, 1); // iterate over result set one row at a time + + while (!select.done()) { + select.execute(); + result.push_back(frontend); + } + + return result; + } + + std::vector<Frontend> queryFrontends() { + if (DEBUG) cout << "dbSqlite::queryFrontends()" << endl; Session session("SQLite", this->dbFile); - std::vector<WeatherConditions> result; - WeatherConditions wc; + std::vector<Frontend> result; + Frontend frontend; Statement select(session); - select << "SELECT * FROM weather_conditions;", - into(wc.condition_id), - into(wc.main), - into(wc.description), + select << "SELECT frontendId, lngCode, plz FROM settings_frontend;", + into(frontend.frontendId), + into(frontend.lngCode), + into(frontend.plz), range(0, 1); // iterate over result set one row at a time while (!select.done()) { select.execute(); - result.push_back(wc); + result.push_back(frontend); } return result; diff --git a/influxdb.h b/influxdb.h index 380f744..be8029f 100644 --- a/influxdb.h +++ b/influxdb.h @@ -11,6 +11,7 @@ #include <cstring> #include <cstdio> #include <cstdlib> +#include <WinSock2.h> #ifdef _WIN32 #define NOMINMAX diff --git a/openweathermap.h b/openweathermap.h index 49bf4f1..d33fb0d 100644 --- a/openweathermap.h +++ b/openweathermap.h @@ -20,22 +20,21 @@ #include "json.h" #include "structs.h" -#define DEBUG false +#define DEBUG true class openweathermap { private: std::string appid; std::string plz; std::string lngCode; - weatherData sWeather; - std::vector<weatherData> vecForecast; + WeatherData sWeather; + std::vector<WeatherData> vecForecast; dbSqlite* db; public: openweathermap(dbSqlite* db_new) { if (DEBUG) std::cout << "openweathermap::openweathermap()" << std::endl; this->db = db_new; - //db = new dbSqlite(); } std::string str_tolower(std::string s) { @@ -46,17 +45,18 @@ public: return s; } - weatherData getSWeather() { + WeatherData getSWeather() { if (DEBUG) std::cout << "openweathermap::getSWeather()" << std::endl; return this->sWeather; } - std::vector<weatherData> getSForecast() { + std::vector<WeatherData> getSForecast() { if (DEBUG) std::cout << "openweathermap::getSForecast()" << std::endl; return this->vecForecast; } void setSWeather(json::JSON jObj, std::string plz, std::string lngCode) { + if (DEBUG) std::cout << "openweathermap::setSWeather(" << plz << ")" << std::endl; this->sWeather.plz = plz; this->sWeather.lngCode = lngCode; @@ -76,14 +76,20 @@ public: this->sWeather.rain3h = jObj["rain"]["3h"].ToFloat(); this->sWeather.snow1h = jObj["snow"]["1h"].ToFloat(); this->sWeather.snow3h = jObj["snow"]["3h"].ToFloat(); - this->sWeather.icons = jObj["weather"].dump(); + this->sWeather.icon1Id = jObj["weather"][0]["id"].ToInt(); + this->sWeather.icon1 = jObj["weather"][0]["icon"].ToString(); + if (jObj["weather"].size() == 2) { + this->sWeather.icon2Id = jObj["weather"][1]["id"].ToInt(); + this->sWeather.icon2 = jObj["weather"][1]["icon"].ToString(); + } db->setSWeather(this->sWeather); } void setSForecast(json::JSON jObj, std::string plz, std::string lngCode) { if (DEBUG) std::cout << "openweathermap::setSForecast(" << plz << ") " << jObj["cnt"] << std::endl; for (int i = 0; i < jObj["cnt"].ToInt(); i++) { - weatherData item; + WeatherData item; + item.plz = plz; item.lngCode = lngCode; item.sunrise = jObj["city"]["sunrise"].ToInt(); @@ -97,8 +103,13 @@ public: item.windSpeed = jObj["list"][i]["wind"]["speed"].ToFloat(); item.windDeg = jObj["list"][i]["wind"]["deg"].ToInt(); item.clouds = jObj["list"][i]["clouds"]["all"].ToInt(); - item.icons = jObj["list"][i]["weather"].dump(); item.from = jObj["list"][i]["dt_txt"].ToString(); + item.icon1Id = jObj["list"][i]["weather"][0]["id"].ToInt(); + item.icon1 = jObj["list"][i]["weather"][0]["icon"].ToString(); + if(jObj["list"][i]["weather"].size() == 2) { + item.icon2Id = jObj["list"][i]["weather"][1]["id"].ToInt(); + item.icon2 = jObj["list"][i]["weather"][1]["icon"].ToString(); + } if (vecForecast.size() != jObj["cnt"].ToInt()) { this->vecForecast.push_back(item); } @@ -167,7 +178,10 @@ public: .field("snow1h", this->sWeather.snow1h) .field("snow3h", this->sWeather.snow3h) .field("clouds", this->sWeather.clouds) - .field("icons", this->sWeather.icons) + .field("icon1Id", this->sWeather.icon1Id) + .field("icon1", this->sWeather.icon1) + .field("icon2Id", this->sWeather.icon2Id) + .field("icon2", this->sWeather.icon2) .post_http(si); } diff --git a/structs.h b/structs.h index 5084c67..63e03d4 100644 --- a/structs.h +++ b/structs.h @@ -1,12 +1,6 @@ #pragma once #include <iostream> -struct WeatherConditions { - int condition_id; - std::string main; - std::string description; -}; - struct Settings { std::string owm_appid; std::string owm_plz; @@ -26,7 +20,7 @@ struct Settings { int mqtt_port; }; -struct weatherData { +struct WeatherData { std::string plz; std::string lngCode; int sunrise; @@ -45,6 +39,20 @@ struct weatherData { double rain3h; double snow1h; double snow3h; - std::string icons; + int icon1Id; + std::string icon1; + int icon2Id; + std::string icon2; std::string from; +}; + +struct Frontend { + std::string frontendId; + std::string plz; + std::string lngCode; +}; + +struct Region { + std::string plz; + std::string lngCode; }; \ No newline at end of file diff --git a/weatherstation.db b/weatherstation.db index 0480848860acbce2f325143c95034c0a5b63b980..ab058e1b6b1f02359d4a1a033614290c92cc6b8e 100644 GIT binary patch delta 1293 zcmb``$y3u%9Ki9=KucTTQ56c}62OHW+CTw!ma0{#VN+b8B`Kh_Ae2Q=%c6BWiR1s^ zc!q<21pW_l@t_`hV#XORe&Yx_gANxn^LcqmetGYg-+NiuC|=koUMX@_NRspt=~zbo zn<_Q0LaHen&CdBO$<CCNwd_pgd~<&P;2O-FcP18HR-3!FHf^yh9tuZ|Nh2N~jSWu@ zL?*^!aU-Vhx{=Y=?QiMzE4?kP9e$-`*YgrbnZv&Qc0gCALlZ+Ip@}MoeLOn1edVZ9 zqOsw&F+I=NzNP3!Bs3L`b1q+7&I(%+;+15zS{I9KHg^vRwv5JfV|LekJDCRbfvLZT zSbnA47Yuau`ISJh-QTb5IZ(owb=b#Zy91T7-=9}bjc3gA75?|~RZ{(b*00<lwj^1s zOJY-`#JahddE)I#?)$b<xzpYpYVd^(eaI6I>ydih6Rz`wb;GNwAvH1-@it}1B2AUe z)@$JdGCU4iS)?SfWsbKq+Q-D7k?FFqdk8d-B_v^m4VkdR9LR|*WFrRxxo}}0_TvEZ zkPkNsP>6#l!XX@%q=a;Y&QT~RMhQxB499T-Cs77-G0Y#J5>=>14Qf#bGkGsm)T04D zG@=R3IEB+VgR{&pouhLe7tn%Mw4oh-T*M_@MgUiE6&>hA5Z9og3*G2JFZysDH_(rp zxP{xegS*Tx-NOI|5keS4&|x5gVT@oD_wfKxJVXp*7{?<_U=ndmVHz`-#beB29#2sH zl;#=CL>91!B`o7PUO<M$v{vB#lASF!*-ERDSQTHzr=6|zI#`*O-syJjwKba?>l;+v zu*<S=s|`M{ui0*)>-E*Es=0nnx94Zbl9trclA0x{$w@6esbwTJYf`hl(=wGG!6FRL delta 7361 zcmdT}iGNd7_D^2+<-JFnwrRSMgsy2?=*!E?OBBR{2#Ty_aT&n0eQ9IU6p~aB#Q0#` zSF9R0P{iF)25}iTltD!XTo7>iISlf1M4g`-GXDHII^%rrdrceA`2&9ad~zP|e82bJ zbI&>V+<PCFe&oIMBkzrsMWZzu%@z3N|LAqiykoRxY-5~;N@riIVHYx=*w=0?8dYT9 zy4K!7WtUPWXJw^U7BkU~gtRoVa!G$GGxX3+o;#&&-W1Qgwn=AA@eC=gjaEy4x^GFm z%M(v#nynV-Jr_n(ojuVM-q5<>tz#hBC8ZP5WLI8k^~v>Glau4L@^TEBO-Re7Xr@O> zE$Qq}cEvOC{$%=>8tOPpeu)#KhT9LB=1KNvJjsDX0#@}!<H@`Qh5D|nl<rK$my%(B zfm?mF`ALmvp6+H3uv^(H*=}|gJC?OD?=!z=E@c)mCMHC$pg*T)&>s5{`UU$Ydpo_+ z9<)1b-`RHC*VxwE`fX#))2**qAF^I<U2GMt|FYUFA6uTZ++zL0(r#I8X)CcUv5d1g zEn55A=FiN3G~aAao0lLw2Ta*wx1MsjsB_vody<l*%3_h|bhQin6TMP$lGG*jX51m@ zcwLREYa57lOR0D|EhQz7sv0eT!0E=QZwsW96i=>{5}7=1S*Bs~8KcqE2Em@7^JZ20 z6gX~aG@2Uf%hSklEdim<=j8QNX@kx)JrPZjpOo@Ui}&?Oi4smvIoduAZjw=kaU z>gboGWLYb;HPk=a(w#lhf`LSDG#MrT95oJlC-uW=1Icto5u;iZ%_rHJQiqgKM44Y{ zymy4uo#^lEl`NJAH5MZ5dGpJf73PNuvpo|X=vW?4p_@@YrPCJ`l_IQ1G-OSve=Uue z^DHVk<myZ`)hlIF9vB!MkW$f9CJvu^R5TV%B<dT$Z=?E#`2UIKX{rbB3@V}W&XVGt zJsEWLtNqs1f;*Aw&U3Rv<hfF}l=NU74UJ!4ql`V25Mck(#FWxx40r<7mG5t;JM~pz zRCvMgQJt!4YAQOgR7#}P4o7>k06FSh6@<&+l0p~Q=Gdw<MvZ1vlj7N(bE5;DJ&LHR zL(3MbM{Rkc`L=dVv8!}AtCRi9-J`+hsk2r7u#>XtJ8?JDXwTR)BT;6?BKJzEuiH^q z$gjB?Hq4?N%2-$cE8|j+gu;{ZM2~lmLTIj1*jDAGUFtzRhGv&8WF!6xqp<??sgzlH z8Mf{?>JYQa1?D*I&kA#@6ifB@q_7HyP<J^{LHmCSI%P#BCG`yttmvwpAaL66m6vuY zTVR-?swf5S*M6tG%!o?qw33y|t9_dWPI|d;9lI;Zc$7u$u5e~U2DwT=@M-_95QZnF zyfKYTW45p}rm-ZTrz$IT^t}F5GTPNY^avO0shab&C$%q4?RWvRMXLU1k0O0WX#OI! zY|};xR0w!ME@unbzENOL(1r`yg^`;Xm(o%~8NZGJRnUeCsKb*hplLL&GG+e%tIT+s z6jK+u^5%EwsVuB(E9m5S>pTTrRk0YcMk^MO3(#^q&`NE~FVN&j(H*HonmSrBe{6ep z#8(9_O?#5cRFhsCcrDuIf^LVVIZPRA)l=0oG@nhCmd7)wJNc)lFP<4ny}rVE)LX!x zp!rngWlv~OFr{&35NBzQ<U7nim5vi?mFpwn8$gUmPSlvtK-3(}6ARX8+LKqhjUbKF z>>qm1rr`1A*sMkvWd<;+HT&`{@=v#3Y57k6eb^Csg--{<WX+%Q@A;=<aE}`LK6SZ! zC{Q`guBq8y^O}+!KCRN*>-k>t&*P`BPz~5$*4V!zyUgY6kL-8s*X-x)$LvA&J$5g< zn|+miiG3DFi^tdp*^TU->@DmK>{aYqb~U@4%`RhmS&3c3p2N1YGuWwY8+$S<u;W-C zTf<hfPPT}(u?FU6<_G2&^Cj~sbC~&%Il#Qd>|%B>FEY<CTbM_g`<c6$+nGV;I_3&S zW>zr+Og|H6I&pSc$jo7;GgFv}OoZW?7N&vmGL=jz!!i~|NB@)lK0zO)|A+pBK16>& z@1x(KchcMG=jo^D&Gf_cCVD-cqu0^b(3jC!`XV|__tD*Sls=E1PtT&ypeNH4Xp!b< zKV3(=X&3FFY1%|<?LXST>+6tG-A$4~=d9tH_PrGj^PJcg&u!eGuXOmjp!RZ1>}oMA z@_ZY%tlTW#Sl;C71mJQpQ}p3bP>c`&r=RFWQYz`|P}(%z^^M4Ltz7Q<)6G@0#zdj? zyO<4+M1n#<;YU3e_NV$h3!{lHwPD5i;8eP(74dLL5IE8&b_*42Spv+*m9EeRgm73G zYz=s}dIS5dS;X<Ozs`dE?Z^F)Yd<C{cg=q{L&}HGfV}jhC6L)emq1=}h@8h)|0N80 z@xcX<tNxsVT>0lB$csKCtycUOX|?==F31btzZi1h{W8eRdje$o-LoN62UbBYJ5a8{ z_)GT_pnu<d$mBcAAp71S-6i%0Aba1Q3wgm?L?3^P=zHEA2ig6`49M7?UPx(=9<pn< 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 Gx&IB-F)V@r -- GitLab