From b4e6d0b0e8a92d11da8727c107493d1e17963107 Mon Sep 17 00:00:00 2001 From: Taras Panechko Date: Fri, 13 Jun 2025 18:12:07 +0300 Subject: [PATCH] add 3 4 5 labs --- .../Panechko_Taras_lab_3/mc_lab_03.ino | 233 ++++++++++++++++++ .../Panechko_Taras_lab_4/mc_lab_04.ino | 233 ++++++++++++++++++ .../src-backend/.cache.sqlite | Bin 0 -> 24576 bytes .../src-backend/__init__.py | 0 .../Panechko_Taras_lab_4/src-backend/app.py | 46 ++++ .../src-backend/controllers/__init__.py | 0 .../src-backend/controllers/conditions.py | 49 ++++ .../src-backend/controllers/mqtt.py | 34 +++ .../src-backend/core/__init__.py | 0 .../src-backend/core/config.py | 40 +++ .../src-backend/db_models/__init__.py | 0 .../src-backend/db_models/conditions.py | 41 +++ .../src-backend/routers/__init__.py | 0 .../src-backend/routers/conditions.py | 28 +++ .../src-backend/whether_condition_records.db | Bin 0 -> 8192 bytes .../Panechko_Taras_lab_5/mc_lab_05.ino | 233 ++++++++++++++++++ .../src-backend/.cache.sqlite | Bin 0 -> 24576 bytes .../src-backend/__init__.py | 0 .../Panechko_Taras_lab_5/src-backend/app.py | 46 ++++ .../src-backend/controllers/__init__.py | 0 .../src-backend/controllers/conditions.py | 49 ++++ .../src-backend/controllers/mqtt.py | 34 +++ .../src-backend/core/__init__.py | 0 .../src-backend/core/config.py | 40 +++ .../src-backend/db_models/__init__.py | 0 .../src-backend/db_models/conditions.py | 41 +++ .../src-backend/routers/__init__.py | 0 .../src-backend/routers/conditions.py | 28 +++ .../src-backend/whether_condition_records.db | Bin 0 -> 8192 bytes .../Panechko_Taras_lab_5/src-web/App.css | 3 + .../Panechko_Taras_lab_5/src-web/App.js | 8 + .../Panechko_Taras_lab_5/src-web/App.test.js | 8 + .../src-web/components/ConditionsPage.js | 67 +++++ .../src-web/components/ui/button.js | 10 + .../src-web/components/ui/card.js | 18 ++ .../Panechko_Taras_lab_5/src-web/index.js | 10 + .../src-web/reportWebVitals.js | 13 + .../src-web/setupTests.js | 5 + .../src-web/styles/ConditionPage.css | 53 ++++ .../src-web/styles/button.css | 14 ++ .../src-web/styles/card.css | 18 ++ 41 files changed, 1402 insertions(+) create mode 100644 mc_labs/mc_lab_03/Panechko_Taras_lab_3/mc_lab_03.ino create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/mc_lab_04.ino create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/.cache.sqlite create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/__init__.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/app.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/__init__.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/conditions.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/mqtt.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/core/__init__.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/core/config.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/db_models/__init__.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/db_models/conditions.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/routers/__init__.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/routers/conditions.py create mode 100644 mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/whether_condition_records.db create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/mc_lab_05.ino create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/.cache.sqlite create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/__init__.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/app.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/__init__.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/conditions.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/mqtt.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/core/__init__.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/core/config.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/db_models/__init__.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/db_models/conditions.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/routers/__init__.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/routers/conditions.py create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/whether_condition_records.db create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/App.css create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/App.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/App.test.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ConditionsPage.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/button.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/card.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/index.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/reportWebVitals.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/setupTests.js create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/ConditionPage.css create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/button.css create mode 100644 mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/card.css diff --git a/mc_labs/mc_lab_03/Panechko_Taras_lab_3/mc_lab_03.ino b/mc_labs/mc_lab_03/Panechko_Taras_lab_3/mc_lab_03.ino new file mode 100644 index 0000000..9dad694 --- /dev/null +++ b/mc_labs/mc_lab_03/Panechko_Taras_lab_3/mc_lab_03.ino @@ -0,0 +1,233 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const char* DEVICE_NAME = "Politechnick_tracker_N1"; +const char* DEVICE_LOCATION = "LVIV"; + +// Wi-Fi +const char* ssid = "Ivanna"; +const char* password = "ivanna0707"; + +// MQTT +const char* mqtt_server = "test.mosquitto.org"; +const int mqtt_port = 1883; +const char* mqtt_topic = "Politechnick_trecker_t_h_Taras/sensors"; + +WiFiClient espClient; +PubSubClient mqttClient(espClient); + +// Flask Server +const char* serverUrl = "http://192.168.0.105:8000/api/v1/conditions?without_local=true"; + +// OLED +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); + +// DHT11 +#define DHTPIN 14 +#define DHTTYPE DHT11 +DHT dht(DHTPIN, DHTTYPE); + +// Дані +String localTemperature = "N/A"; +String localHumidity = "N/A"; +String temperature = "N/A"; +String humidity = "N/A"; + +time_t savedEpoch = 0; +uint32_t lastSyncMillis = 0; +bool wasWiFiConnected = false; + +uint32_t lastScreenUpdate = 0; +uint32_t lastWeatherUpdate = 0; +uint32_t lastMQTTPublish = 0; +const uint32_t screenInterval = 3000; +const uint32_t weatherInterval = 60000; +const uint32_t mqttInterval = 10000; + +// === Display === +void initDisplay() { + Wire.begin(); + if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { + Serial.println("[OLED] Failed to initialize display. Check I2C connection!"); + while (true); + } + display.clearDisplay(); + display.display(); + Serial.println("[OLED] Display initialized"); +} + +void showSensorData() { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + + display.println("Local Sensor:"); + display.print("Temp: "); + display.print(localTemperature); + display.println(" C"); + display.print("Hum: "); + display.print(localHumidity); + display.println(" %"); + + display.println(""); + + display.println("External:"); + display.print("Temp: "); + display.print(temperature); + display.println(" C"); + display.print("Hum: "); + display.print(humidity); + display.println(" %"); + + display.display(); +} + + +// === Wi-Fi & Time === +bool connectToWiFi(uint32_t timeout = 15000) { + WiFi.begin(ssid, password); + Serial.print("[WiFi] Connecting to WiFi"); + uint32_t start = millis(); + while (WiFi.status() != WL_CONNECTED && (millis() - start < timeout)) { + delay(500); Serial.print("."); + } + Serial.println(WiFi.status() == WL_CONNECTED ? "\n[WiFi] Connected!" : "\n[WiFi] Failed to connect."); + return WiFi.status() == WL_CONNECTED; +} + +void syncTimeIfConnected() { + bool now = WiFi.status() == WL_CONNECTED; + if (now && !wasWiFiConnected) { + configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + struct tm timeinfo; + if (getLocalTime(&timeinfo)) { + savedEpoch = mktime(&timeinfo); + lastSyncMillis = millis(); + Serial.println("[Time] Time synchronized"); + } + } + wasWiFiConnected = now; +} + +// === Sensors & HTTP === +void readLocalSensor() { + float t = dht.readTemperature(); + float h = dht.readHumidity(); + Serial.printf("[Sensor] Temp: %.2f, Humidity: %.2f\n", t, h); + if (isnan(t) || isnan(h)) { + localTemperature = "Err"; + localHumidity = "Err"; + Serial.println("[Sensor] Failed to read from DHT"); + } else { + localTemperature = String((uint8_t)t); + localHumidity = String((uint8_t)h); + } +} + +void fetchWeather() { + if (WiFi.status() == WL_CONNECTED) { + Serial.println("[HTTP] Fetching weather..."); + HTTPClient http; + http.begin(espClient, serverUrl); + int code = http.GET(); + Serial.printf("[HTTP] Response code: %d\n", code); + if (code == 200) { + String payload = http.getString(); + StaticJsonDocument<512> doc; + DeserializationError error = deserializeJson(doc, payload); + if (error) { + Serial.print("[HTTP] JSON deserialization failed: "); + Serial.println(error.c_str()); + } else if (doc.size() > 0 && doc[0]["temperature"] && doc[0]["humidity"]) { + temperature = String(doc[0]["temperature"].as(), 1); + humidity = String(doc[0]["humidity"].as(), 1); + Serial.printf("[HTTP] Remote temp: %s, humidity: %s\n", temperature.c_str(), humidity.c_str()); + } else { + Serial.println("[HTTP] No valid data in JSON response"); + temperature = "N/A"; + humidity = "N/A"; + } + } else { + Serial.println("[HTTP] Failed to get weather"); + } + http.end(); + } else { + Serial.println("[HTTP] No WiFi connection"); + temperature = "N/A"; + humidity = "N/A"; + } +} + +// === MQTT === +void reconnectMQTT() { + while (!mqttClient.connected()) { + Serial.print("[MQTT] Connecting... "); + if (mqttClient.connect("ESP8266Client")) { + Serial.println("connected"); + } else { + Serial.printf("failed, rc=%d. Retry in 2s\n", mqttClient.state()); + delay(2000); + } + } +} + +void publishToMQTT() { + if (!mqttClient.connected()) reconnectMQTT(); + StaticJsonDocument<128> doc; + doc["device_name"] = DEVICE_NAME; + doc["location"] = DEVICE_LOCATION; + doc["temperature"] = localTemperature; + doc["humidity"] = localHumidity; + char payload[128]; + serializeJson(doc, payload); + mqttClient.publish(mqtt_topic, payload); + Serial.printf("[MQTT] Published: %s\n", payload); +} + +// === Main === +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("[System] Setup started"); + initDisplay(); + dht.begin(); + connectToWiFi(); + configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + mqttClient.setServer(mqtt_server, mqtt_port); + fetchWeather(); + Serial.println("[System] Setup complete"); +} + +void loop() { + uint32_t now = millis(); + + if (now - lastScreenUpdate > screenInterval) { + readLocalSensor(); + showSensorData(); + lastScreenUpdate = now; + } + + if (now - lastWeatherUpdate > weatherInterval) { + fetchWeather(); + lastWeatherUpdate = now; + } + + if (now - lastMQTTPublish > mqttInterval) { + publishToMQTT(); + lastMQTTPublish = now; + } + + syncTimeIfConnected(); + mqttClient.loop(); +} diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/mc_lab_04.ino b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/mc_lab_04.ino new file mode 100644 index 0000000..9dad694 --- /dev/null +++ b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/mc_lab_04.ino @@ -0,0 +1,233 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const char* DEVICE_NAME = "Politechnick_tracker_N1"; +const char* DEVICE_LOCATION = "LVIV"; + +// Wi-Fi +const char* ssid = "Ivanna"; +const char* password = "ivanna0707"; + +// MQTT +const char* mqtt_server = "test.mosquitto.org"; +const int mqtt_port = 1883; +const char* mqtt_topic = "Politechnick_trecker_t_h_Taras/sensors"; + +WiFiClient espClient; +PubSubClient mqttClient(espClient); + +// Flask Server +const char* serverUrl = "http://192.168.0.105:8000/api/v1/conditions?without_local=true"; + +// OLED +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); + +// DHT11 +#define DHTPIN 14 +#define DHTTYPE DHT11 +DHT dht(DHTPIN, DHTTYPE); + +// Дані +String localTemperature = "N/A"; +String localHumidity = "N/A"; +String temperature = "N/A"; +String humidity = "N/A"; + +time_t savedEpoch = 0; +uint32_t lastSyncMillis = 0; +bool wasWiFiConnected = false; + +uint32_t lastScreenUpdate = 0; +uint32_t lastWeatherUpdate = 0; +uint32_t lastMQTTPublish = 0; +const uint32_t screenInterval = 3000; +const uint32_t weatherInterval = 60000; +const uint32_t mqttInterval = 10000; + +// === Display === +void initDisplay() { + Wire.begin(); + if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { + Serial.println("[OLED] Failed to initialize display. Check I2C connection!"); + while (true); + } + display.clearDisplay(); + display.display(); + Serial.println("[OLED] Display initialized"); +} + +void showSensorData() { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + + display.println("Local Sensor:"); + display.print("Temp: "); + display.print(localTemperature); + display.println(" C"); + display.print("Hum: "); + display.print(localHumidity); + display.println(" %"); + + display.println(""); + + display.println("External:"); + display.print("Temp: "); + display.print(temperature); + display.println(" C"); + display.print("Hum: "); + display.print(humidity); + display.println(" %"); + + display.display(); +} + + +// === Wi-Fi & Time === +bool connectToWiFi(uint32_t timeout = 15000) { + WiFi.begin(ssid, password); + Serial.print("[WiFi] Connecting to WiFi"); + uint32_t start = millis(); + while (WiFi.status() != WL_CONNECTED && (millis() - start < timeout)) { + delay(500); Serial.print("."); + } + Serial.println(WiFi.status() == WL_CONNECTED ? "\n[WiFi] Connected!" : "\n[WiFi] Failed to connect."); + return WiFi.status() == WL_CONNECTED; +} + +void syncTimeIfConnected() { + bool now = WiFi.status() == WL_CONNECTED; + if (now && !wasWiFiConnected) { + configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + struct tm timeinfo; + if (getLocalTime(&timeinfo)) { + savedEpoch = mktime(&timeinfo); + lastSyncMillis = millis(); + Serial.println("[Time] Time synchronized"); + } + } + wasWiFiConnected = now; +} + +// === Sensors & HTTP === +void readLocalSensor() { + float t = dht.readTemperature(); + float h = dht.readHumidity(); + Serial.printf("[Sensor] Temp: %.2f, Humidity: %.2f\n", t, h); + if (isnan(t) || isnan(h)) { + localTemperature = "Err"; + localHumidity = "Err"; + Serial.println("[Sensor] Failed to read from DHT"); + } else { + localTemperature = String((uint8_t)t); + localHumidity = String((uint8_t)h); + } +} + +void fetchWeather() { + if (WiFi.status() == WL_CONNECTED) { + Serial.println("[HTTP] Fetching weather..."); + HTTPClient http; + http.begin(espClient, serverUrl); + int code = http.GET(); + Serial.printf("[HTTP] Response code: %d\n", code); + if (code == 200) { + String payload = http.getString(); + StaticJsonDocument<512> doc; + DeserializationError error = deserializeJson(doc, payload); + if (error) { + Serial.print("[HTTP] JSON deserialization failed: "); + Serial.println(error.c_str()); + } else if (doc.size() > 0 && doc[0]["temperature"] && doc[0]["humidity"]) { + temperature = String(doc[0]["temperature"].as(), 1); + humidity = String(doc[0]["humidity"].as(), 1); + Serial.printf("[HTTP] Remote temp: %s, humidity: %s\n", temperature.c_str(), humidity.c_str()); + } else { + Serial.println("[HTTP] No valid data in JSON response"); + temperature = "N/A"; + humidity = "N/A"; + } + } else { + Serial.println("[HTTP] Failed to get weather"); + } + http.end(); + } else { + Serial.println("[HTTP] No WiFi connection"); + temperature = "N/A"; + humidity = "N/A"; + } +} + +// === MQTT === +void reconnectMQTT() { + while (!mqttClient.connected()) { + Serial.print("[MQTT] Connecting... "); + if (mqttClient.connect("ESP8266Client")) { + Serial.println("connected"); + } else { + Serial.printf("failed, rc=%d. Retry in 2s\n", mqttClient.state()); + delay(2000); + } + } +} + +void publishToMQTT() { + if (!mqttClient.connected()) reconnectMQTT(); + StaticJsonDocument<128> doc; + doc["device_name"] = DEVICE_NAME; + doc["location"] = DEVICE_LOCATION; + doc["temperature"] = localTemperature; + doc["humidity"] = localHumidity; + char payload[128]; + serializeJson(doc, payload); + mqttClient.publish(mqtt_topic, payload); + Serial.printf("[MQTT] Published: %s\n", payload); +} + +// === Main === +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("[System] Setup started"); + initDisplay(); + dht.begin(); + connectToWiFi(); + configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + mqttClient.setServer(mqtt_server, mqtt_port); + fetchWeather(); + Serial.println("[System] Setup complete"); +} + +void loop() { + uint32_t now = millis(); + + if (now - lastScreenUpdate > screenInterval) { + readLocalSensor(); + showSensorData(); + lastScreenUpdate = now; + } + + if (now - lastWeatherUpdate > weatherInterval) { + fetchWeather(); + lastWeatherUpdate = now; + } + + if (now - lastMQTTPublish > mqttInterval) { + publishToMQTT(); + lastMQTTPublish = now; + } + + syncTimeIfConnected(); + mqttClient.loop(); +} diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/.cache.sqlite b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/.cache.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..ebf13f351bb52179d332741e3bedab109c72580e GIT binary patch literal 24576 zcmeI&&rc&|7zgkfT1wL`6cZvG*zhLn5_YA(fi6U{mhOlM3yO8K985FJyrmP`nc10l z2xv?+F)?l?@iJaLdGO>PU}ECM#6VkH!mxW^mf46+SblmH_Q03 zOXK=%W4o@cYrA)f!-+|4MpZ1^1kQ5+-_+p)yttr^woHtCPo zu*_o`)^%*3`|%y&-B!A_KWGb*(_H|C1Yv7 z%qz{slKFA`%kSj|g07FExiHx<99wW(M73|erZOfg=$cZd>7Jk?#Wdw^bj#!k;uNJ` z>E%@A+xJ%$C9JjPeyD!6x4xF9-V@Y5N$q4WxhwgU=6d$pR!eqiFZbSMdWh~hy5*VV zv(wX4MRC_Cd5g+#j?=6enzMIanWmh)o>u9PO7;0DOc@^61veYIh(cB>loqpvrEIZW zFD|Ss7FLS)as?_|SbDWkSSb{uaD=yY*XL%me*fnW|M>0KD=GZpvh%Lval^k;JX7#pp#G`PWYNTaWvkk|z>;szFpv6R8yi>QU;W$8U}YsGIlqAHwrVo z?72}#4?eKmc{T{;YJt>yKCP#EAU{yy%kGJ2Id--`wVyBL%Jhu{*V=mXyOjJ!WPVrZg$n`@fB*y_009U<00Izz00bZa zf$J|Y5zDHV|CN{O?f;)EndjF(Ktuom2tWV=5P$##AOHafKmY;|fWQTTWGt?BkN@TG z|NFm=xzGR^2tWV=5P$##AOHafKmY;|fB*zu907U$kNy9P^E?p=1Rwwb2tWV=5P$## NAOHafK;Z8L{sKVi*P;Lb literal 0 HcmV?d00001 diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/__init__.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/app.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/app.py new file mode 100644 index 0000000..3de95ef --- /dev/null +++ b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/app.py @@ -0,0 +1,46 @@ +import threading +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from fastapi import FastAPI + +import paho.mqtt.client as mqtt +from starlette.middleware.cors import CORSMiddleware + +from controllers.mqtt import on_connect, on_message +from core.config import settings +from routers.conditions import router as conditions_router + + +def start_mqtt(): + client = mqtt.Client() + client.on_connect = on_connect + client.on_message = on_message + client.connect(settings.MQTT_HOST, settings.MQTT_PORT, settings.MQTT_KEEP_ALIVE) + client.loop_forever() + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + settings.create_db_and_tables() + threading.Thread(target=start_mqtt, daemon=True).start() + yield + + +app = FastAPI(lifespan=lifespan) + +origins = [ + "http://localhost:3000", + "http://localhost:8000", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(conditions_router) +#docker start mqtt-broker diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/__init__.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/conditions.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/conditions.py new file mode 100644 index 0000000..9cd036b --- /dev/null +++ b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/conditions.py @@ -0,0 +1,49 @@ +from datetime import datetime, timezone + +import openmeteo_requests +from sqlmodel import Session, select, func + +from core.config import settings +from db_models.conditions import Condition + + +openmeteo = openmeteo_requests.Client(session=settings.get_retry_cache_session()) + + +async def get_prometeo_conditions() -> Condition: + params = {**settings.OPENMETEO_WHETHER_CONDITIONS, **settings.LVIV} + responses = openmeteo.weather_api(settings.OPENMETEO_URL, params=params) + current = responses[0].Current() + temperature = current.Variables(0).Value() + humidity = current.Variables(1).Value() + + return Condition( + device_name="prometeo", + location="Lviv", + temperature=temperature, + humidity=humidity, + created_at=datetime.now(timezone.utc) + ) + + +async def get_conditions( + without_local: bool, + start_date: datetime | None, + end_date: datetime | None, + is_last_updated_values: bool, + session: Session +) -> list[Condition]: + external_condition = await get_prometeo_conditions() + if without_local: + return [external_condition] + + query = select(Condition).where( + *((Condition.created_at >= start_date,) if start_date else ()) + + ((Condition.created_at <= end_date,) if end_date else ()) + + ((Condition.id.in_( + select(func.max(Condition.id)).group_by(Condition.device_name) + ),) if is_last_updated_values else ()) + ) + local_conditions = list(session.exec(query).all()) + local_conditions.append(external_condition) + return local_conditions diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/mqtt.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/mqtt.py new file mode 100644 index 0000000..5727713 --- /dev/null +++ b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/controllers/mqtt.py @@ -0,0 +1,34 @@ +import json +import logging + +from sqlmodel import Session + +from core.config import settings +from db_models.conditions import Condition +from pydantic import ValidationError +from json.decoder import JSONDecodeError + +logger = logging.getLogger(__name__) + + +def on_connect(client, userdata, flags, rc): + logger.info(f"Connected to MQTT broker with result code {rc}") + client.subscribe(settings.MQTT_TOPIC) + logger.info(f"Subscribed to topic: {settings.MQTT_TOPIC}") + + +def on_message(client, userdata, msg): + try: + raw_payload = msg.payload.decode() + data = json.loads(raw_payload) + payload = Condition.model_validate(data) + with Session(settings.get_engine()) as session: + session.add(payload) + session.commit() + logger.info(f"Saved payload from topic '{msg.topic}': {data}") + except JSONDecodeError as e: + logger.error(f"JSON decode error: {e}. Raw message: {msg.payload}") + except ValidationError as e: + logger.error(f"Validation error: {e.errors()}. Payload: {msg.payload}") + except Exception as e: + logger.exception(f"Unexpected error while processing MQTT message: {e}") \ No newline at end of file diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/core/__init__.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/core/config.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/core/config.py new file mode 100644 index 0000000..0b23a3a --- /dev/null +++ b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/core/config.py @@ -0,0 +1,40 @@ +import requests_cache +from pydantic_settings import BaseSettings +from retry_requests import retry +from sqlalchemy import Engine +from sqlmodel import create_engine, Session, SQLModel +from typing import Iterator + + +class Settings(BaseSettings): + DATABASE_URL: str + MQTT_TOPIC: str + MQTT_HOST: str + MQTT_PORT: int + MQTT_KEEP_ALIVE: int + LVIV: dict[str, float] = {"latitude": 49.8397, "longitude": 24.0297} + CACHE_DB_NAME: str + OPENMETEO_URL: str + OPENMETEO_WHETHER_CONDITIONS: dict[str, str] = {"current": "temperature_2m,relative_humidity_2m"} + + + def get_engine(self) -> Engine: + return create_engine(self.DATABASE_URL) + + def get_sqlite_session(self) -> Iterator[Session]: + with Session(self.get_engine()) as session: + yield session + + @staticmethod + def create_db_and_tables() -> None: + SQLModel.metadata.create_all(settings.get_engine()) + + def get_retry_cache_session(self): + cache_session = requests_cache.CachedSession(self.CACHE_DB_NAME, expire_after=3600) + return retry(cache_session, backoff_factor=0.1) + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/db_models/__init__.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/db_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/db_models/conditions.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/db_models/conditions.py new file mode 100644 index 0000000..0cb42eb --- /dev/null +++ b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/db_models/conditions.py @@ -0,0 +1,41 @@ +from sqlmodel import SQLModel, Field, Relationship +from datetime import datetime, timezone + +class Condition(SQLModel, table=True): + __table_args__ = {"extend_existing": True} + + id: int | None = Field(default=None, primary_key=True) + device_name: str + location: str + humidity: float = Field(ge=0, le=100, description="%") + temperature: float + created_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +# class Device(SQLModel, table=True): +# id: int | None = Field(default=None, primary_key=True) +# name: str +# location: str +# description: str | None = None +# conditions: list["Condition"] = Relationship(back_populates="device") +# +# +# class Condition(SQLModel, table=True): +# __table_args__ = {"extend_existing": True} +# +# id: int | None = Field(default=None, primary_key=True) +# device_id: int = Field(foreign_key="device.id") +# humidity: float = Field(ge=0,le=100, description="%") +# temperature: float +# created_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc)) +# +# device: Device = Relationship(back_populates="conditions") +# +# +# class Alert(SQLModel, table=True): +# id: int | None = Field(default=None, primary_key=True) +# device_id: int = Field(foreign_key="device.id") +# message: str +# triggered_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) +# resolved: bool = Field(default=False) +# device: Device = Relationship() diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/routers/__init__.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/routers/conditions.py b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/routers/conditions.py new file mode 100644 index 0000000..a67e4b1 --- /dev/null +++ b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/routers/conditions.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends +from sqlmodel import Session + +from src.controllers.conditions import get_conditions +from src.core.config import settings +from src.db_models.conditions import Condition + + +router = APIRouter(prefix="/api/v1", tags=["conditions"]) + + +@router.get("/conditions") +async def get_condition_records( + start_date: datetime | None = None, + end_date: datetime | None = None, + is_last_updated_values: bool = False, + without_local: bool = False, + session: Session = Depends(settings.get_sqlite_session) +) -> list[Condition]: + return await get_conditions( + start_date=start_date, + end_date=end_date, + is_last_updated_values=is_last_updated_values, + without_local=without_local, + session=session + ) diff --git a/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/whether_condition_records.db b/mc_labs/mc_lab_04/Panechko_Taras_lab_4/src-backend/whether_condition_records.db new file mode 100644 index 0000000000000000000000000000000000000000..bf891ade9f7a50c75cbe0e50e9692fdede39419d GIT binary patch literal 8192 zcmeI0U2hvj6ozfb8^=zZl#h~@v}B=tByF>uGvBko5L1>C)k%v9k$M#wXG^Rmc4gcM zE)m)bBz_b(D{%q;iL(}s8avXWxq*uvd*xlPpE+mVoq1=*-+bNfk7NJoaCF)o`>b@O zR4$jceZN#H89XX@yewrrslL3x&+Iy{m{M@|YZHG}Uhzu!E=r+5p+KQPp+KQPp+KQP zp+KQPp+KQPp+KR)f2Y9DKbC8&>+9uTJ{xzxJBfS4!BKzQ9}dn>D)&24Yd`Y$Tc5Wh z|NLuzqfzf4`8#|2(SxYt?>*Z0_r7hn@A{4UQGC|##fO9LY3v`gI`_Y7UA%uX>~*sc zm)<;naoWcWe)7L;KWbr}uhct^PoKx5?)b$hzT|Fi6nDq*(IIZ-zrcL=cXp!-^yALX zZmaXee;7USH~L4Ljm>Ott}D1V|6?UtiB`PjbU9g$mc3@$Oqx;CTS}LbrD(}pOc#^I zXwhq=jieDZyn0$s>QUWWNEecYXu+$cwWJo+y!muKnUCf@FZB{H^1NzVO{!7Vn@i`C zxoFO-q?M!+VZfsNe-yB~R{HpAx!ig@Ji&j1ddGu)@7dva)a^ZsM~8dRKG-?<^cp2p zY!SHy%m*F{5fTy@p^O2m9C#iA3A8pKn3WDZ3pvI&pqRG8iD$@j1yoq>#Ao>{&Wy6m zi6;;kMG5Lh9e6s?PYOeo1t&gxew<1pEpgx>(_d433470pXAmf%iQzVI;wc0ahy-G{ z^7&7u`!8huS2ISGws#$P5^^0dPCzJo$BCyppoD2f?CpGhZ}pY&M^wmp$Yr1mH< z&j}H>p3l$dFEan5TxaXw1t&g>4~m1dwS0a?KiofHe2@h7n|I)G3!(!s#9;sN^7)ze z +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const char* DEVICE_NAME = "Politechnick_tracker_N1"; +const char* DEVICE_LOCATION = "LVIV"; + +// Wi-Fi +const char* ssid = "Ivanna"; +const char* password = "ivanna0707"; + +// MQTT +const char* mqtt_server = "test.mosquitto.org"; +const int mqtt_port = 1883; +const char* mqtt_topic = "Politechnick_trecker_t_h_Taras/sensors"; + +WiFiClient espClient; +PubSubClient mqttClient(espClient); + +// Flask Server +const char* serverUrl = "http://192.168.0.105:8000/api/v1/conditions?without_local=true"; + +// OLED +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); + +// DHT11 +#define DHTPIN 14 +#define DHTTYPE DHT11 +DHT dht(DHTPIN, DHTTYPE); + +// Дані +String localTemperature = "N/A"; +String localHumidity = "N/A"; +String temperature = "N/A"; +String humidity = "N/A"; + +time_t savedEpoch = 0; +uint32_t lastSyncMillis = 0; +bool wasWiFiConnected = false; + +uint32_t lastScreenUpdate = 0; +uint32_t lastWeatherUpdate = 0; +uint32_t lastMQTTPublish = 0; +const uint32_t screenInterval = 3000; +const uint32_t weatherInterval = 60000; +const uint32_t mqttInterval = 10000; + +// === Display === +void initDisplay() { + Wire.begin(); + if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { + Serial.println("[OLED] Failed to initialize display. Check I2C connection!"); + while (true); + } + display.clearDisplay(); + display.display(); + Serial.println("[OLED] Display initialized"); +} + +void showSensorData() { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + + display.println("Local Sensor:"); + display.print("Temp: "); + display.print(localTemperature); + display.println(" C"); + display.print("Hum: "); + display.print(localHumidity); + display.println(" %"); + + display.println(""); + + display.println("External:"); + display.print("Temp: "); + display.print(temperature); + display.println(" C"); + display.print("Hum: "); + display.print(humidity); + display.println(" %"); + + display.display(); +} + + +// === Wi-Fi & Time === +bool connectToWiFi(uint32_t timeout = 15000) { + WiFi.begin(ssid, password); + Serial.print("[WiFi] Connecting to WiFi"); + uint32_t start = millis(); + while (WiFi.status() != WL_CONNECTED && (millis() - start < timeout)) { + delay(500); Serial.print("."); + } + Serial.println(WiFi.status() == WL_CONNECTED ? "\n[WiFi] Connected!" : "\n[WiFi] Failed to connect."); + return WiFi.status() == WL_CONNECTED; +} + +void syncTimeIfConnected() { + bool now = WiFi.status() == WL_CONNECTED; + if (now && !wasWiFiConnected) { + configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + struct tm timeinfo; + if (getLocalTime(&timeinfo)) { + savedEpoch = mktime(&timeinfo); + lastSyncMillis = millis(); + Serial.println("[Time] Time synchronized"); + } + } + wasWiFiConnected = now; +} + +// === Sensors & HTTP === +void readLocalSensor() { + float t = dht.readTemperature(); + float h = dht.readHumidity(); + Serial.printf("[Sensor] Temp: %.2f, Humidity: %.2f\n", t, h); + if (isnan(t) || isnan(h)) { + localTemperature = "Err"; + localHumidity = "Err"; + Serial.println("[Sensor] Failed to read from DHT"); + } else { + localTemperature = String((uint8_t)t); + localHumidity = String((uint8_t)h); + } +} + +void fetchWeather() { + if (WiFi.status() == WL_CONNECTED) { + Serial.println("[HTTP] Fetching weather..."); + HTTPClient http; + http.begin(espClient, serverUrl); + int code = http.GET(); + Serial.printf("[HTTP] Response code: %d\n", code); + if (code == 200) { + String payload = http.getString(); + StaticJsonDocument<512> doc; + DeserializationError error = deserializeJson(doc, payload); + if (error) { + Serial.print("[HTTP] JSON deserialization failed: "); + Serial.println(error.c_str()); + } else if (doc.size() > 0 && doc[0]["temperature"] && doc[0]["humidity"]) { + temperature = String(doc[0]["temperature"].as(), 1); + humidity = String(doc[0]["humidity"].as(), 1); + Serial.printf("[HTTP] Remote temp: %s, humidity: %s\n", temperature.c_str(), humidity.c_str()); + } else { + Serial.println("[HTTP] No valid data in JSON response"); + temperature = "N/A"; + humidity = "N/A"; + } + } else { + Serial.println("[HTTP] Failed to get weather"); + } + http.end(); + } else { + Serial.println("[HTTP] No WiFi connection"); + temperature = "N/A"; + humidity = "N/A"; + } +} + +// === MQTT === +void reconnectMQTT() { + while (!mqttClient.connected()) { + Serial.print("[MQTT] Connecting... "); + if (mqttClient.connect("ESP8266Client")) { + Serial.println("connected"); + } else { + Serial.printf("failed, rc=%d. Retry in 2s\n", mqttClient.state()); + delay(2000); + } + } +} + +void publishToMQTT() { + if (!mqttClient.connected()) reconnectMQTT(); + StaticJsonDocument<128> doc; + doc["device_name"] = DEVICE_NAME; + doc["location"] = DEVICE_LOCATION; + doc["temperature"] = localTemperature; + doc["humidity"] = localHumidity; + char payload[128]; + serializeJson(doc, payload); + mqttClient.publish(mqtt_topic, payload); + Serial.printf("[MQTT] Published: %s\n", payload); +} + +// === Main === +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("[System] Setup started"); + initDisplay(); + dht.begin(); + connectToWiFi(); + configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov"); + mqttClient.setServer(mqtt_server, mqtt_port); + fetchWeather(); + Serial.println("[System] Setup complete"); +} + +void loop() { + uint32_t now = millis(); + + if (now - lastScreenUpdate > screenInterval) { + readLocalSensor(); + showSensorData(); + lastScreenUpdate = now; + } + + if (now - lastWeatherUpdate > weatherInterval) { + fetchWeather(); + lastWeatherUpdate = now; + } + + if (now - lastMQTTPublish > mqttInterval) { + publishToMQTT(); + lastMQTTPublish = now; + } + + syncTimeIfConnected(); + mqttClient.loop(); +} diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/.cache.sqlite b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/.cache.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..ebf13f351bb52179d332741e3bedab109c72580e GIT binary patch literal 24576 zcmeI&&rc&|7zgkfT1wL`6cZvG*zhLn5_YA(fi6U{mhOlM3yO8K985FJyrmP`nc10l z2xv?+F)?l?@iJaLdGO>PU}ECM#6VkH!mxW^mf46+SblmH_Q03 zOXK=%W4o@cYrA)f!-+|4MpZ1^1kQ5+-_+p)yttr^woHtCPo zu*_o`)^%*3`|%y&-B!A_KWGb*(_H|C1Yv7 z%qz{slKFA`%kSj|g07FExiHx<99wW(M73|erZOfg=$cZd>7Jk?#Wdw^bj#!k;uNJ` z>E%@A+xJ%$C9JjPeyD!6x4xF9-V@Y5N$q4WxhwgU=6d$pR!eqiFZbSMdWh~hy5*VV zv(wX4MRC_Cd5g+#j?=6enzMIanWmh)o>u9PO7;0DOc@^61veYIh(cB>loqpvrEIZW zFD|Ss7FLS)as?_|SbDWkSSb{uaD=yY*XL%me*fnW|M>0KD=GZpvh%Lval^k;JX7#pp#G`PWYNTaWvkk|z>;szFpv6R8yi>QU;W$8U}YsGIlqAHwrVo z?72}#4?eKmc{T{;YJt>yKCP#EAU{yy%kGJ2Id--`wVyBL%Jhu{*V=mXyOjJ!WPVrZg$n`@fB*y_009U<00Izz00bZa zf$J|Y5zDHV|CN{O?f;)EndjF(Ktuom2tWV=5P$##AOHafKmY;|fWQTTWGt?BkN@TG z|NFm=xzGR^2tWV=5P$##AOHafKmY;|fB*zu907U$kNy9P^E?p=1Rwwb2tWV=5P$## NAOHafK;Z8L{sKVi*P;Lb literal 0 HcmV?d00001 diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/__init__.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/app.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/app.py new file mode 100644 index 0000000..3de95ef --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/app.py @@ -0,0 +1,46 @@ +import threading +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from fastapi import FastAPI + +import paho.mqtt.client as mqtt +from starlette.middleware.cors import CORSMiddleware + +from controllers.mqtt import on_connect, on_message +from core.config import settings +from routers.conditions import router as conditions_router + + +def start_mqtt(): + client = mqtt.Client() + client.on_connect = on_connect + client.on_message = on_message + client.connect(settings.MQTT_HOST, settings.MQTT_PORT, settings.MQTT_KEEP_ALIVE) + client.loop_forever() + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + settings.create_db_and_tables() + threading.Thread(target=start_mqtt, daemon=True).start() + yield + + +app = FastAPI(lifespan=lifespan) + +origins = [ + "http://localhost:3000", + "http://localhost:8000", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(conditions_router) +#docker start mqtt-broker diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/__init__.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/conditions.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/conditions.py new file mode 100644 index 0000000..9cd036b --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/conditions.py @@ -0,0 +1,49 @@ +from datetime import datetime, timezone + +import openmeteo_requests +from sqlmodel import Session, select, func + +from core.config import settings +from db_models.conditions import Condition + + +openmeteo = openmeteo_requests.Client(session=settings.get_retry_cache_session()) + + +async def get_prometeo_conditions() -> Condition: + params = {**settings.OPENMETEO_WHETHER_CONDITIONS, **settings.LVIV} + responses = openmeteo.weather_api(settings.OPENMETEO_URL, params=params) + current = responses[0].Current() + temperature = current.Variables(0).Value() + humidity = current.Variables(1).Value() + + return Condition( + device_name="prometeo", + location="Lviv", + temperature=temperature, + humidity=humidity, + created_at=datetime.now(timezone.utc) + ) + + +async def get_conditions( + without_local: bool, + start_date: datetime | None, + end_date: datetime | None, + is_last_updated_values: bool, + session: Session +) -> list[Condition]: + external_condition = await get_prometeo_conditions() + if without_local: + return [external_condition] + + query = select(Condition).where( + *((Condition.created_at >= start_date,) if start_date else ()) + + ((Condition.created_at <= end_date,) if end_date else ()) + + ((Condition.id.in_( + select(func.max(Condition.id)).group_by(Condition.device_name) + ),) if is_last_updated_values else ()) + ) + local_conditions = list(session.exec(query).all()) + local_conditions.append(external_condition) + return local_conditions diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/mqtt.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/mqtt.py new file mode 100644 index 0000000..5727713 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/controllers/mqtt.py @@ -0,0 +1,34 @@ +import json +import logging + +from sqlmodel import Session + +from core.config import settings +from db_models.conditions import Condition +from pydantic import ValidationError +from json.decoder import JSONDecodeError + +logger = logging.getLogger(__name__) + + +def on_connect(client, userdata, flags, rc): + logger.info(f"Connected to MQTT broker with result code {rc}") + client.subscribe(settings.MQTT_TOPIC) + logger.info(f"Subscribed to topic: {settings.MQTT_TOPIC}") + + +def on_message(client, userdata, msg): + try: + raw_payload = msg.payload.decode() + data = json.loads(raw_payload) + payload = Condition.model_validate(data) + with Session(settings.get_engine()) as session: + session.add(payload) + session.commit() + logger.info(f"Saved payload from topic '{msg.topic}': {data}") + except JSONDecodeError as e: + logger.error(f"JSON decode error: {e}. Raw message: {msg.payload}") + except ValidationError as e: + logger.error(f"Validation error: {e.errors()}. Payload: {msg.payload}") + except Exception as e: + logger.exception(f"Unexpected error while processing MQTT message: {e}") \ No newline at end of file diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/core/__init__.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/core/config.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/core/config.py new file mode 100644 index 0000000..0b23a3a --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/core/config.py @@ -0,0 +1,40 @@ +import requests_cache +from pydantic_settings import BaseSettings +from retry_requests import retry +from sqlalchemy import Engine +from sqlmodel import create_engine, Session, SQLModel +from typing import Iterator + + +class Settings(BaseSettings): + DATABASE_URL: str + MQTT_TOPIC: str + MQTT_HOST: str + MQTT_PORT: int + MQTT_KEEP_ALIVE: int + LVIV: dict[str, float] = {"latitude": 49.8397, "longitude": 24.0297} + CACHE_DB_NAME: str + OPENMETEO_URL: str + OPENMETEO_WHETHER_CONDITIONS: dict[str, str] = {"current": "temperature_2m,relative_humidity_2m"} + + + def get_engine(self) -> Engine: + return create_engine(self.DATABASE_URL) + + def get_sqlite_session(self) -> Iterator[Session]: + with Session(self.get_engine()) as session: + yield session + + @staticmethod + def create_db_and_tables() -> None: + SQLModel.metadata.create_all(settings.get_engine()) + + def get_retry_cache_session(self): + cache_session = requests_cache.CachedSession(self.CACHE_DB_NAME, expire_after=3600) + return retry(cache_session, backoff_factor=0.1) + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/db_models/__init__.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/db_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/db_models/conditions.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/db_models/conditions.py new file mode 100644 index 0000000..0cb42eb --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/db_models/conditions.py @@ -0,0 +1,41 @@ +from sqlmodel import SQLModel, Field, Relationship +from datetime import datetime, timezone + +class Condition(SQLModel, table=True): + __table_args__ = {"extend_existing": True} + + id: int | None = Field(default=None, primary_key=True) + device_name: str + location: str + humidity: float = Field(ge=0, le=100, description="%") + temperature: float + created_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +# class Device(SQLModel, table=True): +# id: int | None = Field(default=None, primary_key=True) +# name: str +# location: str +# description: str | None = None +# conditions: list["Condition"] = Relationship(back_populates="device") +# +# +# class Condition(SQLModel, table=True): +# __table_args__ = {"extend_existing": True} +# +# id: int | None = Field(default=None, primary_key=True) +# device_id: int = Field(foreign_key="device.id") +# humidity: float = Field(ge=0,le=100, description="%") +# temperature: float +# created_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc)) +# +# device: Device = Relationship(back_populates="conditions") +# +# +# class Alert(SQLModel, table=True): +# id: int | None = Field(default=None, primary_key=True) +# device_id: int = Field(foreign_key="device.id") +# message: str +# triggered_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) +# resolved: bool = Field(default=False) +# device: Device = Relationship() diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/routers/__init__.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/routers/conditions.py b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/routers/conditions.py new file mode 100644 index 0000000..a67e4b1 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/routers/conditions.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends +from sqlmodel import Session + +from src.controllers.conditions import get_conditions +from src.core.config import settings +from src.db_models.conditions import Condition + + +router = APIRouter(prefix="/api/v1", tags=["conditions"]) + + +@router.get("/conditions") +async def get_condition_records( + start_date: datetime | None = None, + end_date: datetime | None = None, + is_last_updated_values: bool = False, + without_local: bool = False, + session: Session = Depends(settings.get_sqlite_session) +) -> list[Condition]: + return await get_conditions( + start_date=start_date, + end_date=end_date, + is_last_updated_values=is_last_updated_values, + without_local=without_local, + session=session + ) diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/whether_condition_records.db b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-backend/whether_condition_records.db new file mode 100644 index 0000000000000000000000000000000000000000..bf891ade9f7a50c75cbe0e50e9692fdede39419d GIT binary patch literal 8192 zcmeI0U2hvj6ozfb8^=zZl#h~@v}B=tByF>uGvBko5L1>C)k%v9k$M#wXG^Rmc4gcM zE)m)bBz_b(D{%q;iL(}s8avXWxq*uvd*xlPpE+mVoq1=*-+bNfk7NJoaCF)o`>b@O zR4$jceZN#H89XX@yewrrslL3x&+Iy{m{M@|YZHG}Uhzu!E=r+5p+KQPp+KQPp+KQP zp+KQPp+KQPp+KR)f2Y9DKbC8&>+9uTJ{xzxJBfS4!BKzQ9}dn>D)&24Yd`Y$Tc5Wh z|NLuzqfzf4`8#|2(SxYt?>*Z0_r7hn@A{4UQGC|##fO9LY3v`gI`_Y7UA%uX>~*sc zm)<;naoWcWe)7L;KWbr}uhct^PoKx5?)b$hzT|Fi6nDq*(IIZ-zrcL=cXp!-^yALX zZmaXee;7USH~L4Ljm>Ott}D1V|6?UtiB`PjbU9g$mc3@$Oqx;CTS}LbrD(}pOc#^I zXwhq=jieDZyn0$s>QUWWNEecYXu+$cwWJo+y!muKnUCf@FZB{H^1NzVO{!7Vn@i`C zxoFO-q?M!+VZfsNe-yB~R{HpAx!ig@Ji&j1ddGu)@7dva)a^ZsM~8dRKG-?<^cp2p zY!SHy%m*F{5fTy@p^O2m9C#iA3A8pKn3WDZ3pvI&pqRG8iD$@j1yoq>#Ao>{&Wy6m zi6;;kMG5Lh9e6s?PYOeo1t&gxew<1pEpgx>(_d433470pXAmf%iQzVI;wc0ahy-G{ z^7&7u`!8huS2ISGws#$P5^^0dPCzJo$BCyppoD2f?CpGhZ}pY&M^wmp$Yr1mH< z&j}H>p3l$dFEan5TxaXw1t&g>4~m1dwS0a?KiofHe2@h7n|I)G3!(!s#9;sN^7)ze z; +} + +export default App; diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/App.test.js b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ConditionsPage.js b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ConditionsPage.js new file mode 100644 index 0000000..4b76bb1 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ConditionsPage.js @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { Card, CardContent } from "./ui/card"; +import { format } from "date-fns"; +import "../styles/ConditionPage.css"; + +const API_BASE = "http://localhost:8000/api/v1/conditions"; + +export default function ConditionsPage() { + const [conditions, setConditions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [avgTemp, setAvgTemp] = useState(null); + + useEffect(() => { + fetchConditions(); + }, []); + + const fetchConditions = async () => { + setLoading(true); + setError(""); + try { + const response = await axios.get(API_BASE, { + params: { + is_last_updated_values: true, + }, + }); + const data = response.data; + setConditions(data); + const avg = data.reduce((sum, c) => sum + c.temperature, 0) / data.length; + setAvgTemp(avg); + } catch (err) { + setError("Failed to fetch conditions"); + } finally { + setLoading(false); + } + }; + + const getFontClass = () => { + if (avgTemp === null) return "default-font"; + if (avgTemp < 0) return "cold-font"; + if (avgTemp < 15) return "cool-font"; + if (avgTemp < 30) return "warm-font"; + return "hot-font"; + }; + + return ( +
+

Current Weather Conditions

+ {loading &&

Loading...

} + {error &&

{error}

} +
+ {conditions.map((cond) => ( + + +

{cond.device_name}

+

Location: {cond.location}

+

Temperature: {cond.temperature} °C

+

Humidity: {cond.humidity} %

+

Time: {format(new Date(cond.created_at), 'yyyy-MM-dd HH:mm:ss')}

+
+
+ ))} +
+
+ ); +} diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/button.js b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/button.js new file mode 100644 index 0000000..91f5ab6 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/button.js @@ -0,0 +1,10 @@ +import React from "react"; +import "../../styles/button.css" + +export function Button({ children, onClick }) { + return ( + + ); +} diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/card.js b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/card.js new file mode 100644 index 0000000..617d909 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/components/ui/card.js @@ -0,0 +1,18 @@ +import React from "react"; +import "../../styles/card.css" + +export function Card({ children }) { + return ( +
+ {children} +
+ ); +} + +export function CardContent({ children }) { + return ( +
+ {children} +
+ ); +} diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/index.js b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/index.js new file mode 100644 index 0000000..4db0035 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); \ No newline at end of file diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/reportWebVitals.js b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/setupTests.js b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/ConditionPage.css b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/ConditionPage.css new file mode 100644 index 0000000..fc96806 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/ConditionPage.css @@ -0,0 +1,53 @@ +@import url("https://fonts.googleapis.com/css2?family=Bangers&family=Poppins:wght@400;600&family=Roboto+Slab&display=swap"); + + +.page-container { + padding: 2rem; + min-height: 100vh; + background: linear-gradient(to bottom, #e6ffe6, #ffffff); +} + +.page-container h1 { + text-align: center; + font-size: 2rem; + color: #166534; + margin-bottom: 1rem; +} + +.cards-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + margin-top: 1rem; +} + +@media (min-width: 768px) { + .cards-grid { + grid-template-columns: 1fr 1fr; + } +} + +.error { + color: #dc2626; + font-weight: bold; +} + +.default-font { + font-family: sans-serif; +} + +.cold-font { + font-family: "Courier New", monospace; +} + +.cool-font { + font-family: "Roboto Slab", serif; +} + +.warm-font { + font-family: "Poppins", sans-serif; +} + +.hot-font { + font-family: "Bangers", cursive; +} diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/button.css b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/button.css new file mode 100644 index 0000000..6802712 --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/button.css @@ -0,0 +1,14 @@ +.green-button { + background-color: #22c55e; + color: white; + padding: 0.5rem 1rem; + font-weight: bold; + border: none; + border-radius: 0.5rem; + cursor: pointer; + transition: background-color 0.2s ease-in-out; +} + +.green-button:hover { + background-color: #15803d; +} diff --git a/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/card.css b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/card.css new file mode 100644 index 0000000..791986b --- /dev/null +++ b/mc_labs/mc_lab_05/Panechko_Taras_lab_5/src-web/styles/card.css @@ -0,0 +1,18 @@ +.card { + background-color: #f0fdf4; + border: 2px solid #a7f3d0; + border-radius: 1rem; + box-shadow: 0 4px 6px rgba(0, 128, 0, 0.2); + padding: 1rem; + transition: transform 0.2s ease-in-out; +} + +.card:hover { + transform: scale(1.02); +} + +.card-content { + display: flex; + flex-direction: column; + gap: 0.25rem; +}