commit 5e53d50e2bc3b2d4eedb133b5a6ad4809a2fb78c Author: alifridho Date: Tue Aug 12 10:18:15 2025 +0700 first commit diff --git a/Bismillah_Tugas_Akhir_Selesai.ino b/Bismillah_Tugas_Akhir_Selesai.ino new file mode 100644 index 0000000..a0c0725 --- /dev/null +++ b/Bismillah_Tugas_Akhir_Selesai.ino @@ -0,0 +1,176 @@ +#include +#include +#include +#include +#include "DHT.h" +#include "HX711.h" +#include + +const char* ssid = "kiskz"; +const char* password = "123443210"; +const char* mqtt_server = "192.168.43.11"; + +WiFiClient espClient; +PubSubClient client(espClient); + +LiquidCrystal_I2C lcd(0x27, 16, 2); + +#define DHTPIN 15 +#define DHTTYPE DHT22 +DHT dht(DHTPIN, DHTTYPE); + +#define HX711_DT 18 +#define HX711_SCK 19 +HX711 scale; + +#define RELAY_KIPAS 4 +#define RELAY_PTC 5 +#define RELAY_ON LOW +#define RELAY_OFF HIGH + +#define SOIL_PIN 35 +const int soilWet = 1180; +const int soilDry = 2495; + +bool autoMode = false; +float minThreshold = 60.0; +float maxThreshold = 90.0; +const float localThreshold = 90.0; +const unsigned long relayDuration = 30000; + +bool relayActive = false; +unsigned long relayStartTime = 0; + +float temperature = 0; +float humidity = 0; +float weight = 0; +int soilRaw = 0; +int soilPercent = 0; + +void setup_wifi() { + delay(10); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + } +} + +void callback(char* topic, byte* payload, unsigned int length) { + StaticJsonDocument<256> doc; + DeserializationError error = deserializeJson(doc, payload, length); + if (error) return; + + if (doc.containsKey("auto")) { + autoMode = (String(doc["auto"]) == "ON"); + } + + if (doc.containsKey("min")) minThreshold = doc["min"]; + if (doc.containsKey("max")) maxThreshold = doc["max"]; +} + +void reconnect() { + while (!client.connected()) { + if (client.connect("ESP32Client")) { + client.subscribe("iot/control/relay"); + } else { + delay(5000); + } + } +} + +void setup() { + Serial.begin(115200); + Wire.begin(21, 22); + dht.begin(); + lcd.init(); + lcd.backlight(); + + pinMode(RELAY_KIPAS, OUTPUT); + pinMode(RELAY_PTC, OUTPUT); + digitalWrite(RELAY_KIPAS, RELAY_OFF); + digitalWrite(RELAY_PTC, RELAY_OFF); + + scale.begin(HX711_DT, HX711_SCK); + scale.set_scale(-452.1357); + scale.tare(); + + setup_wifi(); + client.setServer(mqtt_server, 1883); + client.setCallback(callback); +} + +void loop() { + if (!client.connected()) { + reconnect(); + } + client.loop(); + + temperature = dht.readTemperature(); + humidity = dht.readHumidity(); + weight = scale.get_units(5); + soilRaw = analogRead(SOIL_PIN); + soilPercent = map(soilRaw, soilDry, soilWet, 0, 100); + soilPercent = constrain(soilPercent, 0, 100); + + if (autoMode) { + if (!relayActive && humidity > maxThreshold) { + digitalWrite(RELAY_KIPAS, RELAY_ON); + digitalWrite(RELAY_PTC, RELAY_ON); + relayActive = true; + relayStartTime = millis(); + } + if (relayActive && humidity <= minThreshold) { + digitalWrite(RELAY_KIPAS, RELAY_OFF); + digitalWrite(RELAY_PTC, RELAY_OFF); + relayActive = false; + } + } else { + if (!relayActive && humidity > localThreshold) { + digitalWrite(RELAY_KIPAS, RELAY_ON); + digitalWrite(RELAY_PTC, RELAY_ON); + relayActive = true; + relayStartTime = millis(); + } + if (relayActive && millis() - relayStartTime >= relayDuration) { + digitalWrite(RELAY_KIPAS, RELAY_OFF); + digitalWrite(RELAY_PTC, RELAY_OFF); + relayActive = false; + } + } + + lcd.setCursor(0, 0); + lcd.print("H:"); + lcd.print(humidity, 1); + lcd.print("% "); + lcd.print("W:"); + lcd.print((int)weight); + lcd.print("g "); + + lcd.setCursor(0, 1); + if (relayActive) { + if (!autoMode) { + unsigned long remaining = (relayDuration - (millis() - relayStartTime)) / 1000; + lcd.print("H:"); + lcd.print(remaining); + lcd.print("s "); + } else { + lcd.print("H:Auto "); + } + } else { + lcd.print("H:Idle "); + } + lcd.print("S:"); + lcd.print(soilPercent); + lcd.print("% "); + + StaticJsonDocument<256> doc; + doc["humidity"] = humidity; + doc["weight"] = weight; + doc["soil"] = soilPercent; + + char buffer[256]; + size_t n = serializeJson(doc, buffer); + client.publish("iot/sensor", buffer, n); + + delay(1000); +} diff --git a/flows.json b/flows.json new file mode 100644 index 0000000..91ad963 --- /dev/null +++ b/flows.json @@ -0,0 +1,1444 @@ +[ + { + "id": "tab1", + "type": "tab", + "label": "Dashboard Demo", + "disabled": false, + "info": "" + }, + { + "id": "5f31529b3e749ebe", + "type": "mqtt in", + "z": "tab1", + "name": "MQTT Sensor", + "topic": "iot/sensor", + "qos": "0", + "datatype": "auto", + "broker": "mqtt_broker_local", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 300, + "y": 580, + "wires": [ + [ + "e81e5db8c5529bb0" + ] + ] + }, + { + "id": "e81e5db8c5529bb0", + "type": "json", + "z": "tab1", + "name": "Parse JSON", + "property": "payload", + "action": "", + "pretty": false, + "x": 480, + "y": 580, + "wires": [ + [ + "a4575ed03e736231", + "94dd8dde94285a13" + ] + ] + }, + { + "id": "37d04d285e6d7864", + "type": "function", + "z": "tab1", + "name": " Get Data Filter", + "func": "// Simpan pilihan ke flow untuk digunakan tombol ambil data\nflow.set(\"sensor_selected\", msg.payload);\nreturn null;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 540, + "y": 780, + "wires": [ + [] + ] + }, + { + "id": "a4575ed03e736231", + "type": "function", + "z": "tab1", + "name": "Split Sensor Data", + "func": "return [\n { payload: msg.payload.weight },\n { payload: msg.payload.humidity },\n { payload: msg.payload.soil }\n];", + "outputs": 3, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 690, + "y": 560, + "wires": [ + [ + "1815f02556163f1d", + "6452681ddc0cd6cd" + ], + [ + "edf401a2960b5562", + "22b8f2858da8ce95" + ], + [ + "8992134a9099bffa", + "e03f9616d12ff1df" + ] + ] + }, + { + "id": "5a0b35725a51d4dc", + "type": "sqlite", + "z": "tab1", + "mydb": "sqlite_local", + "sqlquery": "msg.topic", + "sql": "", + "name": "Simpan ke SQLite", + "x": 810, + "y": 700, + "wires": [ + [] + ] + }, + { + "id": "1815f02556163f1d", + "type": "ui_gauge", + "z": "tab1", + "name": "Weight", + "group": "ui_group_main", + "order": 2, + "width": 6, + "height": 0, + "gtype": "gage", + "title": "Weight (g)", + "label": "g", + "format": "{{value}}", + "min": 0, + "max": "200", + "colors": [ + "#00b500", + "#e6e600", + "#ca3838" + ], + "seg1": "300", + "seg2": "700", + "diff": false, + "className": "", + "x": 890, + "y": 520, + "wires": [] + }, + { + "id": "edf401a2960b5562", + "type": "ui_gauge", + "z": "tab1", + "name": "Humidity", + "group": "ui_group_main", + "order": 1, + "width": 6, + "height": 0, + "gtype": "gage", + "title": "Humidity (%)", + "label": "%", + "format": "{{value}}", + "min": 0, + "max": "100", + "colors": [ + "#00b500", + "#e6e600", + "#ca3838" + ], + "seg1": "40", + "seg2": "70", + "diff": false, + "className": "", + "x": 900, + "y": 560, + "wires": [] + }, + { + "id": "8992134a9099bffa", + "type": "ui_gauge", + "z": "tab1", + "name": "Soil Moisture", + "group": "ui_group_main", + "order": 3, + "width": 6, + "height": 0, + "gtype": "gage", + "title": "Soil Moisture", + "label": "%", + "format": "{{value}}", + "min": 0, + "max": 100, + "colors": [ + "#00b500", + "#e6e600", + "#ca3838" + ], + "seg1": "30", + "seg2": "60", + "diff": false, + "className": "", + "x": 910, + "y": 600, + "wires": [] + }, + { + "id": "6452681ddc0cd6cd", + "type": "ui_chart", + "z": "tab1", + "name": "Chart Weight", + "group": "ui_group_main", + "order": 5, + "width": 6, + "height": 0, + "label": "Weight History", + "chartType": "line", + "legend": "false", + "xformat": "HH:mm:ss", + "interpolate": "linear", + "nodata": "Waiting...", + "dot": false, + "ymin": "0", + "ymax": "1000", + "removeOlder": "3", + "removeOlderPoints": "100", + "removeOlderUnit": "60", + "cutout": 0, + "useOneColor": false, + "useUTC": false, + "colors": [ + "#1f77b4", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000" + ], + "outputs": 1, + "useDifferentColor": false, + "className": "", + "x": 1110, + "y": 520, + "wires": [ + [] + ] + }, + { + "id": "22b8f2858da8ce95", + "type": "ui_chart", + "z": "tab1", + "name": "Chart Humidity", + "group": "ui_group_main", + "order": 4, + "width": 6, + "height": 0, + "label": "Humidity History", + "chartType": "line", + "legend": "false", + "xformat": "HH:mm:ss", + "interpolate": "linear", + "nodata": "Waiting...", + "dot": false, + "ymin": "0", + "ymax": "100", + "removeOlder": "3", + "removeOlderPoints": "100", + "removeOlderUnit": "60", + "cutout": 0, + "useOneColor": false, + "useUTC": false, + "colors": [ + "#ff7f0e", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000" + ], + "outputs": 1, + "useDifferentColor": false, + "className": "", + "x": 1120, + "y": 560, + "wires": [ + [] + ] + }, + { + "id": "e03f9616d12ff1df", + "type": "ui_chart", + "z": "tab1", + "name": "Chart Soil", + "group": "ui_group_main", + "order": 6, + "width": 6, + "height": 0, + "label": "Soil Moisture History", + "chartType": "line", + "legend": "false", + "xformat": "HH:mm:ss", + "interpolate": "linear", + "nodata": "Waiting...", + "dot": false, + "ymin": "0", + "ymax": "100", + "removeOlder": "3", + "removeOlderPoints": "100", + "removeOlderUnit": "60", + "cutout": 0, + "useOneColor": false, + "useUTC": false, + "colors": [ + "#2ca02c", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000", + "#000000" + ], + "outputs": 1, + "useDifferentColor": false, + "className": "", + "x": 1130, + "y": 600, + "wires": [ + [] + ] + }, + { + "id": "d3167a81e47894f4", + "type": "cronplus", + "z": "tab1", + "name": "Cron Setiap 10 Menit", + "outputField": "payload", + "timeZone": "", + "storeName": "", + "commandResponseMsgOutput": "output1", + "defaultLocation": "", + "defaultLocationType": "default", + "outputs": 1, + "options": [ + { + "name": "cron1menit", + "topic": "filter/weight", + "payloadType": "default", + "payload": "", + "expressionType": "cron", + "expression": "0 */10 * * * *", + "location": "", + "offset": "0", + "solarType": "all", + "solarEvents": "sunrise,sunset" + } + ], + "x": 340, + "y": 700, + "wires": [ + [ + "1b56edf14dd48a3e" + ] + ] + }, + { + "id": "1b56edf14dd48a3e", + "type": "function", + "z": "tab1", + "name": "Ambil dan Simpan DB", + "func": "var data = flow.get(\"last_sensor_data\");\n\nif (data) {\n msg.topic = `INSERT INTO sensor_data (weight, humidity, soil, timestamp) \n VALUES (${data.weight}, ${data.humidity}, ${data.soil}, datetime('now', 'localtime'))`;\n return msg;\n} else {\n return null;\n}\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 580, + "y": 700, + "wires": [ + [ + "5a0b35725a51d4dc" + ] + ] + }, + { + "id": "94dd8dde94285a13", + "type": "function", + "z": "tab1", + "name": "Simpan data di Flow", + "func": "flow.set(\"last_sensor_data\", msg.payload);\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 640, + "y": 640, + "wires": [ + [] + ] + }, + { + "id": "bbbaab907e1d9ba1", + "type": "ui_dropdown", + "z": "tab1", + "name": "", + "label": "Pilih Data Sensor ", + "tooltip": "", + "place": "Select option", + "group": "a5d4d4b9.0e5d58", + "order": 1, + "width": 0, + "height": 0, + "passthru": true, + "multiple": false, + "options": [ + { + "label": "Weight", + "value": "weight", + "type": "str" + }, + { + "label": "Humidity", + "value": "humidity", + "type": "str" + }, + { + "label": "Soil", + "value": "soil", + "type": "str" + } + ], + "payload": "", + "topic": "topic", + "topicType": "msg", + "className": "", + "x": 330, + "y": 780, + "wires": [ + [ + "37d04d285e6d7864" + ] + ] + }, + { + "id": "f302ee7fa549388d", + "type": "ui_button", + "z": "tab1", + "name": "Ambil Data", + "group": "a5d4d4b9.0e5d58", + "order": 5, + "width": 0, + "height": 0, + "passthru": false, + "label": "Lihat Data 1 Jam Terakhir", + "tooltip": "", + "color": "", + "bgcolor": "", + "className": "", + "icon": "", + "payload": "", + "payloadType": "str", + "topic": "topic", + "topicType": "msg", + "x": 330, + "y": 840, + "wires": [ + [ + "9dea1ff0914d9df1" + ] + ] + }, + { + "id": "9dea1ff0914d9df1", + "type": "function", + "z": "tab1", + "name": "Ambil Data Sensor", + "func": "let sensor = flow.get(\"sensor_selected\") || \"weight\"; // default berat\nmsg.topic = `\n SELECT timestamp, ${sensor} AS value\n FROM sensor_data\n WHERE timestamp >= datetime('now', '-1 hour', 'localtime')\n ORDER BY timestamp ASC\n`;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 530, + "y": 840, + "wires": [ + [ + "b3c205b6f30ee3a1" + ] + ] + }, + { + "id": "9c2ff6f5414057d6", + "type": "function", + "z": "tab1", + "name": "Ubah Format Chart", + "func": "let labels = [];\nlet values = [];\n\nmsg.payload.forEach(row => {\n labels.push(row.timestamp);\n values.push(row.value);\n});\n\nreturn {\n payload: [{\n series: [\"Sensor\"],\n data: [values],\n labels: labels\n }]\n};\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 910, + "y": 800, + "wires": [ + [ + "a1e5f5cd0e18215a" + ] + ] + }, + { + "id": "b3c205b6f30ee3a1", + "type": "sqlite", + "z": "tab1", + "mydb": "sqlite_local", + "sqlquery": "msg.topic", + "sql": "", + "name": "Ambil Data", + "x": 730, + "y": 840, + "wires": [ + [ + "9c2ff6f5414057d6", + "f29389be4bd7f771" + ] + ] + }, + { + "id": "a1e5f5cd0e18215a", + "type": "ui_chart", + "z": "tab1", + "name": "Chart 1 Jam Terakhir", + "group": "a5d4d4b9.0e5d58", + "order": 3, + "width": "9", + "height": "9", + "label": "chart", + "chartType": "line", + "legend": "false", + "xformat": "HH:mm:ss", + "interpolate": "linear", + "nodata": "", + "dot": false, + "ymin": "", + "ymax": "", + "removeOlder": 1, + "removeOlderPoints": "", + "removeOlderUnit": "3600", + "cutout": 0, + "useOneColor": false, + "useUTC": false, + "colors": [ + "#1f77b4", + "#aec7e8", + "#ff7f0e", + "#2ca02c", + "#98df8a", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5" + ], + "outputs": 1, + "useDifferentColor": false, + "className": "", + "x": 1140, + "y": 800, + "wires": [ + [] + ] + }, + { + "id": "f29389be4bd7f771", + "type": "ui_table", + "z": "tab1", + "group": "a5d4d4b9.0e5d58", + "name": "Tabel Data Sensor 1 Jam", + "order": 6, + "width": "9", + "height": "9", + "columns": [], + "outputs": 0, + "cts": false, + "x": 930, + "y": 880, + "wires": [] + }, + { + "id": "1faca8d668b6c953", + "type": "mqtt in", + "z": "tab1", + "name": "MQTT IN", + "topic": "iot/sensor", + "qos": "2", + "datatype": "auto-detect", + "broker": "mqtt_broker_local", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 260, + "y": 920, + "wires": [ + [ + "43e20a522799a074" + ] + ] + }, + { + "id": "43e20a522799a074", + "type": "function", + "z": "tab1", + "name": "Logika Otomasi", + "func": "let auto = flow.get(\"autoMode\") || \"OFF\";\nlet min = flow.get(\"minThreshold\") || 60;\nlet max = flow.get(\"maxThreshold\") || 90;\nlet humidity = msg.payload.humidity;\n\nif (auto === \"ON\") {\n if (humidity > max || (humidity <= max && humidity >= min)) {\n return {\n payload: {\n kipas: \"ON\",\n ptc: \"ON\"\n }\n };\n } else if (humidity < min) {\n return {\n payload: {\n kipas: \"OFF\",\n ptc: \"OFF\"\n }\n };\n }\n}\n\nreturn null; // Auto OFF → ESP32 logika lokal\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 500, + "y": 980, + "wires": [ + [ + "5c1f51279e899535" + ] + ] + }, + { + "id": "5c1f51279e899535", + "type": "mqtt out", + "z": "tab1", + "name": "MQTT OUT", + "topic": "iot/control/relay", + "qos": "0", + "retain": "true", + "respTopic": "", + "contentType": "", + "userProps": "", + "correl": "", + "expiry": "", + "broker": "mqtt_broker_local", + "x": 730, + "y": 920, + "wires": [] + }, + { + "id": "d89d2771a3657743", + "type": "ui_switch", + "z": "tab1", + "name": "Auto Mode", + "label": "Auto Mode", + "tooltip": "", + "group": "077642f35af224dd", + "order": 1, + "width": 0, + "height": 0, + "passthru": true, + "decouple": "false", + "topic": "auto", + "topicType": "str", + "style": "", + "onvalue": "ON", + "onvalueType": "str", + "onicon": "", + "oncolor": "", + "offvalue": "OFF", + "offvalueType": "str", + "officon": "", + "offcolor": "", + "animate": false, + "className": "", + "x": 270, + "y": 980, + "wires": [ + [ + "e6969d414614df2a" + ] + ] + }, + { + "id": "e6969d414614df2a", + "type": "function", + "z": "tab1", + "name": "Set Auto Mode", + "func": "flow.set(\"autoMode\", msg.payload);\n\nlet mode = msg.payload;\n\nif (mode === \"ON\") {\n let min = flow.get(\"minThreshold\") || 60;\n let max = flow.get(\"maxThreshold\") || 90;\n\n return {\n payload: {\n auto: \"ON\",\n min: min,\n max: max\n }\n };\n} else {\n return {\n payload: {\n auto: \"OFF\"\n }\n };\n}\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 520, + "y": 1040, + "wires": [ + [ + "c648c08be1a9d688" + ] + ] + }, + { + "id": "c648c08be1a9d688", + "type": "mqtt out", + "z": "tab1", + "name": "MQTT OUT", + "topic": "iot/control/relay", + "qos": "0", + "retain": "true", + "respTopic": "", + "contentType": "", + "userProps": "", + "correl": "", + "expiry": "", + "broker": "mqtt_broker_local", + "x": 730, + "y": 980, + "wires": [] + }, + { + "id": "300d03d320030023", + "type": "ui_numeric", + "z": "tab1", + "name": "", + "label": "Min Threshold", + "tooltip": "", + "group": "077642f35af224dd", + "order": 2, + "width": 0, + "height": 0, + "wrap": false, + "passthru": true, + "topic": "min", + "topicType": "str", + "format": "{{value}}", + "min": 0, + "max": "100", + "step": 1, + "className": "", + "x": 280, + "y": 1040, + "wires": [ + [ + "176a870347715cfa" + ] + ] + }, + { + "id": "1157d77839be828e", + "type": "ui_numeric", + "z": "tab1", + "name": "Max Threshold", + "label": "Max Threshold", + "tooltip": "", + "group": "077642f35af224dd", + "order": 3, + "width": 0, + "height": 0, + "wrap": false, + "passthru": true, + "topic": "max", + "topicType": "str", + "format": "{{value}}", + "min": 0, + "max": "100", + "step": 1, + "className": "", + "x": 280, + "y": 1100, + "wires": [ + [ + "bcea903c811a2674" + ] + ] + }, + { + "id": "176a870347715cfa", + "type": "function", + "z": "tab1", + "name": "MIn Threshold", + "func": "flow.set(\"minThreshold\", msg.payload);\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 520, + "y": 1100, + "wires": [ + [] + ] + }, + { + "id": "bcea903c811a2674", + "type": "function", + "z": "tab1", + "name": "Max Threshold", + "func": "flow.set(\"maxThreshold\", msg.payload);\nreturn null;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 500, + "y": 1160, + "wires": [ + [] + ] + }, + { + "id": "eabf72d94dda646b", + "type": "ui_form", + "z": "tab1", + "name": "", + "label": "Input Tanggal untuk Cek Tabel Hasil Sensor", + "group": "a5d4d4b9.0e5d58", + "order": 10, + "width": 0, + "height": 0, + "options": [ + { + "label": "StartDate", + "value": "StartDate", + "type": "date", + "required": true, + "rows": null + }, + { + "label": "EndDate", + "value": "EndDate", + "type": "date", + "required": true, + "rows": null + } + ], + "formValue": { + "StartDate": "", + "EndDate": "" + }, + "payload": "", + "submit": "Load Data", + "cancel": "Cancel", + "topic": "topic", + "topicType": "msg", + "splitLayout": "", + "className": "", + "x": 890, + "y": 1080, + "wires": [ + [ + "a924537ad40c9a35" + ] + ] + }, + { + "id": "a924537ad40c9a35", + "type": "function", + "z": "tab1", + "name": "function 1", + "func": "var start = msg.payload.StartDate;\nvar end = msg.payload.EndDate;\n\nmsg.topic = `SELECT ID, timestamp, weight, humidity, soil\n FROM sensor_data\n WHERE timestamp BETWEEN '${start}' AND '${end}'\n ORDER BY timestamp ASC`;\n\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1180, + "y": 1080, + "wires": [ + [ + "94ab216e7402fe83" + ] + ] + }, + { + "id": "94ab216e7402fe83", + "type": "sqlite", + "z": "tab1", + "mydb": "sqlite_local", + "sqlquery": "msg.topic", + "sql": "", + "name": "", + "x": 980, + "y": 1160, + "wires": [ + [ + "06b6b6875d1ef97f", + "009cc5fdaef1059b" + ] + ] + }, + { + "id": "06b6b6875d1ef97f", + "type": "ui_table", + "z": "tab1", + "group": "a5d4d4b9.0e5d58", + "name": "", + "order": 11, + "width": "18", + "height": "9", + "columns": [], + "outputs": 0, + "cts": false, + "x": 1310, + "y": 1160, + "wires": [] + }, + { + "id": "0e1cb4193c91927d", + "type": "ui_chart", + "z": "tab1", + "name": "", + "group": "", + "order": 12, + "width": "9", + "height": "9", + "label": "Grafik Hasil Sensor", + "chartType": "line", + "legend": "true", + "xformat": "HH:mm", + "interpolate": "linear", + "nodata": "", + "dot": false, + "ymin": "", + "ymax": "", + "removeOlder": "24", + "removeOlderPoints": "", + "removeOlderUnit": "3600", + "cutout": 0, + "useOneColor": false, + "useUTC": false, + "colors": [ + "#1f77b4", + "#aec7e8", + "#ff7f0e", + "#2ca02c", + "#98df8a", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5" + ], + "outputs": 1, + "useDifferentColor": false, + "className": "", + "x": 3730, + "y": 2780, + "wires": [ + [] + ] + }, + { + "id": "d40f0c435b9f0739", + "type": "function", + "z": "tab1", + "name": "function 2", + "func": "let hasil = [];\nlet terakhirJam = null;\n\nfor (let row of msg.payload) {\n let waktu = new Date(row.timestamp);\n let jam = waktu.getHours();\n\n if (jam !== terakhirJam) {\n let t = waktu.getTime();\n hasil.push(\n { topic: \"weight\", payload: row.weight, x: t },\n { topic: \"humidity\", payload: row.humidity, x: t },\n { topic: \"soil\", payload: row.soil, x: t }\n );\n terakhirJam = jam;\n }\n}\n\nreturn [hasil];\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3740, + "y": 2620, + "wires": [ + [] + ] + }, + { + "id": "009cc5fdaef1059b", + "type": "debug", + "z": "tab1", + "name": "debug 1", + "active": true, + "tosidebar": true, + "console": true, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 1010, + "y": 1220, + "wires": [] + }, + { + "id": "44417d6d5c88b067", + "type": "debug", + "z": "tab1", + "name": "debug 2", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 3900, + "y": 2640, + "wires": [] + }, + { + "id": "e24142f27563c8f7", + "type": "function", + "z": "tab1", + "name": "function 3", + "func": "let hasil = [];\n\nfor (let row of msg.payload) {\n let waktu = new Date(row.timestamp);\n let t = waktu.getTime(); // ubah ke milidetik\n\n hasil.push(\n { topic: \"weight\", payload: row.weight, x: t },\n { topic: \"humidity\", payload: row.humidity, x: t },\n { topic: \"soil\", payload: row.soil, x: t }\n );\n}\n\nreturn [hasil];\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3740, + "y": 2680, + "wires": [ + [] + ] + }, + { + "id": "2274b4168fa225ab", + "type": "ui_button", + "z": "tab1", + "name": "Clear Chart", + "group": "", + "order": 13, + "width": 0, + "height": 0, + "passthru": false, + "label": "Clear Chart", + "tooltip": "", + "color": "", + "bgcolor": "", + "className": "", + "icon": "", + "payload": "", + "payloadType": "str", + "topic": "topic", + "topicType": "msg", + "x": 3510, + "y": 2920, + "wires": [ + [] + ] + }, + { + "id": "f6a49940e4446c3e", + "type": "function", + "z": "tab1", + "name": "function 6", + "func": "return [\n { topic: \"weight\", payload: [] },\n { topic: \"humidity\", payload: [] },\n { topic: \"soil\", payload: [] }\n];\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3520, + "y": 2840, + "wires": [ + [] + ] + }, + { + "id": "21536e5562e29615", + "type": "function", + "z": "tab1", + "name": "filter 2jam", + "func": "let hasil = [];\nlet terakhirWaktu = null;\nconst intervalMs = 60 * 60 * 1000; // 1 jam = 3600000 ms\n\nfor (let row of msg.payload) {\n let waktu = new Date(row.timestamp);\n if (isNaN(waktu.getTime())) continue;\n\n let t = waktu.getTime();\n\n // Ambil hanya satu data per jam\n if (terakhirWaktu === null || (t - terakhirWaktu) >= intervalMs) {\n hasil.push(\n { topic: \"weight\", payload: row.weight, x: t },\n { topic: \"humidity\", payload: row.humidity, x: t },\n { topic: \"soil\", payload: row.soil, x: t }\n );\n terakhirWaktu = t;\n }\n}\n\nreturn [hasil];\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 3520, + "y": 2780, + "wires": [ + [] + ] + }, + { + "id": "c6422797ed00af54", + "type": "ui_form", + "z": "tab1", + "name": "", + "label": "Input Waktu untuk Hitung Durasi Nyala Alat", + "group": "cfa831704ecb774e", + "order": 1, + "width": 0, + "height": 0, + "options": [ + { + "label": "Waktu Nyala Awal", + "value": "start_date", + "type": "date", + "required": true, + "rows": null + }, + { + "label": "Waktu Nyala Akhir", + "value": "end_date", + "type": "date", + "required": true, + "rows": null + } + ], + "formValue": { + "start_date": "", + "end_date": "" + }, + "payload": "", + "submit": "submit", + "cancel": "cancel", + "topic": "topic", + "topicType": "msg", + "splitLayout": "", + "className": "", + "x": 310, + "y": 1260, + "wires": [ + [ + "6ab3a01515f3839f" + ] + ] + }, + { + "id": "6ab3a01515f3839f", + "type": "function", + "z": "tab1", + "name": "query database cari waktu", + "func": "let start = msg.payload.start_date;\nlet end = msg.payload.end_date;\n\nmsg.topic = `\n SELECT timestamp, weight FROM sensor_data \n WHERE timestamp BETWEEN '${start} 00:00:00' AND '${end} 23:59:59' \n ORDER BY timestamp ASC;\n`;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 670, + "y": 1260, + "wires": [ + [ + "cf6c9c8b56973d03" + ] + ] + }, + { + "id": "cf6c9c8b56973d03", + "type": "sqlite", + "z": "tab1", + "mydb": "sqlite_local", + "sqlquery": "msg.topic", + "sql": "", + "name": "", + "x": 500, + "y": 1340, + "wires": [ + [ + "6d4b3c89d262ec1b" + ] + ] + }, + { + "id": "6d4b3c89d262ec1b", + "type": "function", + "z": "tab1", + "name": "hitung waktu", + "func": "let data = msg.payload;\n\nif (!Array.isArray(data) || data.length < 2) {\n msg.payload = \"Data tidak cukup untuk menghitung durasi pengeringan.\";\n return msg;\n}\n\n// Ganti spasi jadi T agar bisa dibaca Date()\nlet waktu1 = new Date(data[0].timestamp.replace(\" \", \"T\"));\nlet waktu2 = new Date(data[data.length - 1].timestamp.replace(\" \", \"T\"));\n\nif (isNaN(waktu1) || isNaN(waktu2)) {\n msg.payload = \"Format waktu tidak valid dari database.\";\n return msg;\n}\n\nlet selisihMs = Math.abs(waktu2 - waktu1);\nlet jam = Math.floor(selisihMs / (1000 * 60 * 60));\nlet menit = Math.floor((selisihMs % (1000 * 60 * 60)) / (1000 * 60));\n\nmsg.payload = `Durasi Lama Menyala Alat: ${jam} jam ${menit} menit`;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 2, + "initialize": "", + "finalize": "", + "libs": [], + "x": 330, + "y": 1420, + "wires": [ + [ + "d7165d9095fce54c" + ] + ] + }, + { + "id": "d7165d9095fce54c", + "type": "ui_text_input", + "z": "tab1", + "name": "Durasi Alat Menyala", + "label": "", + "tooltip": "", + "group": "cfa831704ecb774e", + "order": 2, + "width": "14", + "height": "2", + "passthru": true, + "mode": "text", + "delay": 300, + "topic": "topic", + "sendOnBlur": true, + "className": "", + "topicType": "msg", + "x": 600, + "y": 1420, + "wires": [ + [] + ] + }, + { + "id": "1d392868362ac611", + "type": "ui_form", + "z": "tab1", + "name": "", + "label": "Input Berat Awal dan Akhir", + "group": "cfa831704ecb774e", + "order": 3, + "width": 0, + "height": 0, + "options": [ + { + "label": "Berat Awal", + "value": "berat_awal", + "type": "number", + "required": true, + "rows": null + }, + { + "label": "Berat Akhir", + "value": "berat_akhir", + "type": "number", + "required": true, + "rows": null + } + ], + "formValue": { + "berat_awal": "", + "berat_akhir": "" + }, + "payload": "", + "submit": "submit", + "cancel": "cancel", + "topic": "topic", + "topicType": "msg", + "splitLayout": "", + "className": "", + "x": 360, + "y": 1500, + "wires": [ + [ + "f9c308cbef39eb24" + ] + ] + }, + { + "id": "f9c308cbef39eb24", + "type": "function", + "z": "tab1", + "name": "query database cari waktu", + "func": "let berat_awal = msg.payload.berat_awal;\nlet berat_akhir = msg.payload.berat_akhir;\n\nmsg.topic = `\n SELECT timestamp, weight \n FROM sensor_data \n WHERE weight IN (${berat_awal}, ${berat_akhir})\n ORDER BY timestamp ASC;\n`;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 670, + "y": 1500, + "wires": [ + [ + "36ad9d1719f51b71" + ] + ] + }, + { + "id": "36ad9d1719f51b71", + "type": "sqlite", + "z": "tab1", + "mydb": "sqlite_local", + "sqlquery": "msg.topic", + "sql": "", + "name": "", + "x": 500, + "y": 1580, + "wires": [ + [ + "4dc76c949c967437" + ] + ] + }, + { + "id": "4dc76c949c967437", + "type": "function", + "z": "tab1", + "name": "hitung waktu", + "func": "let data = msg.payload;\n\nif (data.length < 2) {\n msg.payload = \"Tidak ditemukan dua titik berat dalam database.\";\n return msg;\n}\n\n// Ubah ke format Date (pastikan format timestamp dari DB: 'YYYY-MM-DD HH:MM:SS')\nlet waktu1 = new Date(data[0].timestamp.replace(\" \", \"T\"));\nlet waktu2 = new Date(data[1].timestamp.replace(\" \", \"T\"));\n\nif (isNaN(waktu1) || isNaN(waktu2)) {\n msg.payload = \"Waktu tidak valid.\";\n return msg;\n}\n\nlet selisihMs = Math.abs(waktu2 - waktu1);\nlet jam = Math.floor(selisihMs / (1000 * 60 * 60));\nlet menit = Math.floor((selisihMs % (1000 * 60 * 60)) / (1000 * 60));\n\nmsg.payload = `Durasi Lama Proses Pengeringan: ${jam} jam ${menit} menit`;\nreturn msg;\n", + "outputs": 1, + "timeout": 0, + "noerr": 2, + "initialize": "", + "finalize": "", + "libs": [], + "x": 330, + "y": 1660, + "wires": [ + [ + "2ddc72cd58c4a8b2" + ] + ] + }, + { + "id": "2ddc72cd58c4a8b2", + "type": "ui_text_input", + "z": "tab1", + "name": "Durasi Pengering", + "label": "", + "tooltip": "", + "group": "cfa831704ecb774e", + "order": 4, + "width": "14", + "height": "2", + "passthru": true, + "mode": "text", + "delay": 300, + "topic": "topic", + "sendOnBlur": true, + "className": "", + "topicType": "msg", + "x": 590, + "y": 1660, + "wires": [ + [] + ] + }, + { + "id": "mqtt_broker_local", + "type": "mqtt-broker", + "name": "Local MQTT", + "broker": "localhost", + "port": "1883", + "clientid": "", + "autoConnect": true, + "usetls": false, + "protocolVersion": 4, + "keepalive": "60", + "cleansession": true, + "autoUnsubscribe": true, + "birthTopic": "", + "birthQos": "0", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + }, + { + "id": "sqlite_local", + "type": "sqlitedb", + "db": "C:\\Users\\Moch Alif Ridho A\\Documents\\sensor_data.db", + "mode": "RWC" + }, + { + "id": "ui_group_main", + "type": "ui_group", + "name": "Sensor Monitor", + "tab": "ui_tab_main", + "order": 1, + "disp": true, + "width": "18", + "collapse": false, + "className": "" + }, + { + "id": "a5d4d4b9.0e5d58", + "type": "ui_group", + "name": "Filter Data Sensor", + "tab": "c75362e0.9e6a38", + "order": 1, + "disp": true, + "width": "18", + "collapse": false, + "className": "" + }, + { + "id": "077642f35af224dd", + "type": "ui_group", + "name": "Kontrol Relay", + "tab": "c94c9feca08755da", + "order": 1, + "disp": true, + "width": "18", + "collapse": false, + "className": "" + }, + { + "id": "cfa831704ecb774e", + "type": "ui_group", + "name": "Hitung Lama Pengeringan", + "tab": "2e2f751f9fd061a0", + "order": 1, + "disp": true, + "width": "18", + "collapse": false, + "className": "" + }, + { + "id": "ui_tab_main", + "type": "ui_tab", + "name": "Dashboard Monitoring", + "icon": "dashboard", + "order": 2, + "disabled": false, + "hidden": false + }, + { + "id": "c75362e0.9e6a38", + "type": "ui_tab", + "name": "Dashboard Database ", + "icon": "dashboard", + "order": 3, + "disabled": false, + "hidden": false + }, + { + "id": "c94c9feca08755da", + "type": "ui_tab", + "name": "Otomasi Relay", + "icon": "dashboard", + "order": 5, + "disabled": false, + "hidden": false + }, + { + "id": "2e2f751f9fd061a0", + "type": "ui_tab", + "name": "Hitung Durasi Proses Pengeringan", + "icon": "dashboard", + "order": 5, + "disabled": false, + "hidden": false + } +] \ No newline at end of file