From 956746588315422284aed899c1a83c9f9fead488 Mon Sep 17 00:00:00 2001 From: Sasa Karanovic Date: Sat, 10 Jul 2021 17:40:43 -0400 Subject: [PATCH 1/3] Initial commit --- .../Firmware/CentralUnit/CentralUnit.ino | 31 ++++- .../CentralUnit/PlantSystem_MainLogic.ino | 111 ++++++++++++++---- 2 files changed, 119 insertions(+), 23 deletions(-) diff --git a/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino b/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino index 1a51330..c22eebc 100644 --- a/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino +++ b/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino @@ -13,6 +13,9 @@ int WiFi_status = WL_IDLE_STATUS; volatile uint32_t nFlowSensorCount = 0; volatile uint32_t nFlowSensorCount_last = 0; +char dbgBuff[4024] = {0}; +uint32_t dbgBuffPos = 0; + void IRAM_ATTR ISR_flowSensor() { nFlowSensorCount++; } @@ -61,7 +64,20 @@ void setup() setupWebServer(); server.begin(); - attachInterrupt(FLOW_SENSOR_PIN, ISR_flowSensor, FALLING); + flowInterruptEnabled(true); +} + + +void flowInterruptEnabled(bool state) +{ + if(state) + { + attachInterrupt(FLOW_SENSOR_PIN, ISR_flowSensor, FALLING); + } + else + { + detachInterrupt(FLOW_SENSOR_PIN); + } } @@ -259,3 +275,16 @@ void setupWebServer(void) }); } + +void LED_Blink(uint8_t nTimes, uint16_t n_delayPeriod) +{ + while(nTimes--) + { + digitalWrite(LED_PIN, HIGH); + delay(n_delayPeriod); + + digitalWrite(LED_PIN, LOW); + delay(n_delayPeriod); + } +} + diff --git a/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino b/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino index 2a6b096..312140b 100644 --- a/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino +++ b/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino @@ -42,7 +42,7 @@ uint8_t gnSolenoidsOnBusCnt = 0; // Plant watering definitions #define WATERING_VOLUME_MINIMUM 10 #define WATERING_VOLUME_MAXIMUM 550 -#define WATERING_FLOW_EDGES_PER_ML 4 +#define WATERING_FLOW_EDGES_PER_L 5880 //98 // PlantSystem_tick periods #define PERIOD_RESCAN_BUS 24*3600*1000 // Every 24h @@ -66,6 +66,7 @@ uint32_t nTimeout = 0; void PlantSystem_tick(void) { + bool bWateringError = false; esp_task_wdt_reset(); // -- Rescan I2C bus @@ -124,15 +125,23 @@ void PlantSystem_tick(void) // -- Process watering request if(bWatering_requestPending || bWatering_inProgress) { + // Watering logic + // Pump creates high pressure inside watering system. Because of this + // at the beginning of watering process we will first, open valve, then turn on pump. + // When we are done, we will first turn off pump and then solenoid. + // + // In previous version, water pump was too weak so we turned on the pump first to keep the pressure on + // and prevent water from "backing up" (i.e letting air in from plant side). + // With peristaltic pump we can open the solenoids first and still keep air out of the system. + // + Serial.println("bWatering_requestPending | bWatering_inProgress"); // Transition to in progress bWatering_inProgress = true; bWatering_requestPending = false; + nFlowSensorCount = 0; - // Note: Solenoid address and volume must have been validated - Serial.println("Turning pump ON"); - digitalWrite(WATER_PUMP_EN_PIN, HIGH); - delay(200); + // - Open solenoid Serial.println("Energizing solenoid"); if (PlantSystem_SetSolenoidState(nWatering_i2cAddress, true) != true) { @@ -141,33 +150,83 @@ void PlantSystem_tick(void) digitalWrite(WATER_PUMP_EN_PIN, LOW); Serial.println("Turning pump OFF"); Serial.println("Failed to open solenoid. Aborting..."); + LED_Blink(4, 250); return; } + + // - Reset flow counter + nFlowSensorCount = 0; + nFlowSensorCount_last = 0; + flowInterruptEnabled(true); + + // - Turn ON pump + Serial.println("Turning pump ON"); + delay(200); + digitalWrite(WATER_PUMP_EN_PIN, HIGH); + esp_task_wdt_reset(); - nTimeout = millis() + 10000; - while(nFlowSensorCount < (WATERING_FLOW_EDGES_PER_ML*nWatering_volume)) + nTimeout = millis() + 10000; // 10sec time-out for watering + + // Calculate water flow + uint32_t nWaterFlow_millis_timestamp = millis(); + float flowRate = 0.0; + unsigned int flowMilliLitres =0; + unsigned long totalMilliLitres = 0; + + while(millis() < nTimeout) { - if(millis() > nTimeout) + if((millis() - nWaterFlow_millis_timestamp) > 1000) // Only process counters once per second { - break; + flowInterruptEnabled(false); + + nFlowSensorCount_last = nFlowSensorCount; + nFlowSensorCount = 0; + bWateringError = true; + + + flowRate = ((1000.0 / (millis() - nWaterFlow_millis_timestamp)) * nFlowSensorCount_last) / WATERING_FLOW_EDGES_PER_L; + flowMilliLitres = (flowRate / 60) * 1000; + totalMilliLitres += flowMilliLitres; + + if(totalMilliLitres >= nWatering_volume) + { + break; + } + + nWaterFlow_millis_timestamp = millis(); + esp_task_wdt_reset(); + flowInterruptEnabled(true); } } esp_task_wdt_reset(); - Serial.println("De-Energizing solenoid"); - PlantSystem_SetSolenoidState(nWatering_i2cAddress, false); - delay(200); + // - Turn OFF pump digitalWrite(WATER_PUMP_EN_PIN, LOW); Serial.println("Turning pump OFF"); + delay(200); + + // - Close solenoid + Serial.println("De-Energizing solenoid"); + PlantSystem_SetSolenoidState(nWatering_i2cAddress, false); + // Store last flow data and prepare for new request - nFlowSensorCount_last = nFlowSensorCount; + nFlowSensorCount_last = 0; nFlowSensorCount = 0; + nWatering_volume = 0; // Reset request bWatering_inProgress = false; bWatering_requestPending = false; + if(bWateringError) + { + LED_Blink(3, 250); + } + else + { + LED_Blink(2, 500); + } } @@ -610,10 +669,8 @@ void PlantSystem_RequestPrimeSystem(void) bool PlantSystem_PrimeSystemWithWater(void) { - // Turn ON Pump - Serial.println("Turning pump ON"); - digitalWrite(WATER_PUMP_EN_PIN, HIGH); - delay(1000); + + bool bSolenoidOpen = false; // Turn ON all solenoids Serial.println("Energizing ALL solenoids"); @@ -623,6 +680,7 @@ bool PlantSystem_PrimeSystemWithWater(void) { Serial.print("Openned solenoid 0x"); Serial.println(gSolenoidsOnBus[i].i2cAddress, HEX); + bSolenoidOpen = true; } else { @@ -630,11 +688,25 @@ bool PlantSystem_PrimeSystemWithWater(void) Serial.println(gSolenoidsOnBus[i].i2cAddress, HEX); } delay(500); + + // Only turn on pump AFTER solenoid has oppened + if(bSolenoidOpen) + { + // Turn ON Pump + Serial.println("Turning pump ON"); + digitalWrite(WATER_PUMP_EN_PIN, HIGH); + delay(1000); + } } // Let the water flow for a while delay(5000); + // Turn OFF pump + delay(1000); + Serial.println("Turning pump OFF"); + digitalWrite(WATER_PUMP_EN_PIN, LOW); + // Turn OFF all solenoids Serial.println("De-energizing ALL solenoids"); for(uint8_t i=0; i Date: Sat, 10 Jul 2021 18:48:03 -0400 Subject: [PATCH 2/3] Adding http support to calibrate flow sensor --- .../Firmware/CentralUnit/CentralUnit.ino | 48 +++++++++++++++++++ .../CentralUnit/PlantSystem_MainLogic.ino | 29 +++++------ 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino b/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino index c22eebc..1e51b29 100644 --- a/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino +++ b/Part2_Central_board/Firmware/CentralUnit/CentralUnit.ino @@ -1,3 +1,4 @@ +#include #include "WiFi.h" #include "ESPAsyncWebServer.h" #include @@ -7,6 +8,7 @@ #include "Wire.h" #define WDT_TIMEOUT 20000 +#define EEPROM_SIZE 4 AsyncWebServer server(80); int WiFi_status = WL_IDLE_STATUS; @@ -15,6 +17,7 @@ volatile uint32_t nFlowSensorCount_last = 0; char dbgBuff[4024] = {0}; uint32_t dbgBuffPos = 0; +float fImpulsePerML = 0; void IRAM_ATTR ISR_flowSensor() { nFlowSensorCount++; @@ -40,6 +43,21 @@ void setup() Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); + // EEPROM begin + EEPROM.begin(EEPROM_SIZE); + + // Debug EEPROM before read + Serial.println("Before read: fImpulsePerML"); + Serial.println((float)(fImpulsePerML),4); + + // Read flow value from EEPROM + fImpulsePerML = EEPROM.readFloat(0); + + // Debug EEPROM after read + Serial.println("After read: fImpulsePerML"); + Serial.println((float)(fImpulsePerML),4); + + // Configure WDT Serial.println("Configuring WDT..."); esp_task_wdt_init(WDT_TIMEOUT, true); //enable panic so ESP32 restarts @@ -273,6 +291,36 @@ void setupWebServer(void) request->send(200, "text/plain", "OK"); return; }); + + // Calibrate flow sensor + server.on("/calibrateFlowSensor", HTTP_GET, [] (AsyncWebServerRequest *request) { + Serial.println("Calibrate edges per mL"); + + if ( request->hasParam("edges") ) + { + String xEdges; + float fEdges = 0; + + xEdges = request->getParam("edges")->value(); + fEdges = xEdges.toFloat(); + + // Update value + fImpulsePerML = fEdges; + EEPROM.writeFloat(0, fEdges);//EEPROM.put(address, param); + EEPROM.commit(); + + Serial.print("Changing fImpulsePerML to "); + Serial.println((float)(fImpulsePerML),4); + + request->send(200, "text/plain", "OK"); + return; + } + else + { + request->send(404, "text/plain", "MISSING_ARGUMENT"); + return; + } + }); } diff --git a/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino b/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino index 312140b..7c9f2a7 100644 --- a/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino +++ b/Part2_Central_board/Firmware/CentralUnit/PlantSystem_MainLogic.ino @@ -161,8 +161,8 @@ void PlantSystem_tick(void) flowInterruptEnabled(true); // - Turn ON pump + bWateringError = true; Serial.println("Turning pump ON"); - delay(200); digitalWrite(WATER_PUMP_EN_PIN, HIGH); esp_task_wdt_reset(); @@ -170,31 +170,32 @@ void PlantSystem_tick(void) // Calculate water flow uint32_t nWaterFlow_millis_timestamp = millis(); - float flowRate = 0.0; - unsigned int flowMilliLitres =0; + float flowMilliLitres = 0.0; unsigned long totalMilliLitres = 0; + // Wait until target mL or timeout is reached while(millis() < nTimeout) { - if((millis() - nWaterFlow_millis_timestamp) > 1000) // Only process counters once per second + // Calculate mL every second + if( millis() >= nWaterFlow_millis_timestamp) { + // Disable interrupts flowInterruptEnabled(false); - nFlowSensorCount_last = nFlowSensorCount; - nFlowSensorCount = 0; - bWateringError = true; - - - flowRate = ((1000.0 / (millis() - nWaterFlow_millis_timestamp)) * nFlowSensorCount_last) / WATERING_FLOW_EDGES_PER_L; - flowMilliLitres = (flowRate / 60) * 1000; + // Update mL + flowMilliLitres = nFlowSensorCount/fImpulsePerML; totalMilliLitres += flowMilliLitres; + // Check if target has been reached if(totalMilliLitres >= nWatering_volume) { + bWateringError = false; break; } - - nWaterFlow_millis_timestamp = millis(); + + // Update for next iteration + nWaterFlow_millis_timestamp = millis() + 1000; + nFlowSensorCount = 0; esp_task_wdt_reset(); flowInterruptEnabled(true); } @@ -221,7 +222,7 @@ void PlantSystem_tick(void) bWatering_requestPending = false; if(bWateringError) { - LED_Blink(3, 250); + LED_Blink(4, 250); } else { From 1a4afa199b5f541d90222f1958b1386b38cb05c6 Mon Sep 17 00:00:00 2001 From: Sasa Karanovic Date: Fri, 22 Oct 2021 00:24:05 -0400 Subject: [PATCH 3/3] Adding part 3 source files --- .../grafana_dashboard/dashboard.json | 632 ++++++++++++++++++ .../mysql/plantsystem.sql | 112 ++++ .../n8n/PlantSystem_-_Periodic_Watering.json | 319 +++++++++ .../n8n/PlantSystem_-_Read_Sensor_Data.json | 186 ++++++ .../n8n/PlantSystem_-_Water_All_Plants.json | 298 +++++++++ .../PlantSystem_-_Water_Specific_Plant.json | 208 ++++++ README.md | 11 +- 7 files changed, 1764 insertions(+), 2 deletions(-) create mode 100644 Part3_Web_and_Automation/grafana_dashboard/dashboard.json create mode 100644 Part3_Web_and_Automation/mysql/plantsystem.sql create mode 100644 Part3_Web_and_Automation/n8n/PlantSystem_-_Periodic_Watering.json create mode 100644 Part3_Web_and_Automation/n8n/PlantSystem_-_Read_Sensor_Data.json create mode 100644 Part3_Web_and_Automation/n8n/PlantSystem_-_Water_All_Plants.json create mode 100644 Part3_Web_and_Automation/n8n/PlantSystem_-_Water_Specific_Plant.json diff --git a/Part3_Web_and_Automation/grafana_dashboard/dashboard.json b/Part3_Web_and_Automation/grafana_dashboard/dashboard.json new file mode 100644 index 0000000..c8a0d7d --- /dev/null +++ b/Part3_Web_and_Automation/grafana_dashboard/dashboard.json @@ -0,0 +1,632 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 9, + "iteration": 1634868145960, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "MySQL - Plant System", + "description": "", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.4", + "pointradius": 2, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "format": "time_series", + "group": [], + "metricColumn": "sensorID", + "rawQuery": true, + "rawSql": "SELECT\r\n UNIX_TIMESTAMP(`insertTime`) AS `time`,\r\n `sensor_links`.`plantName` AS `metric`,\r\n `sensor_data`.`T` as `value` \r\nFROM `sensor_data`\r\nLEFT JOIN `sensor_links` ON\r\n`sensor_data`.`address` = `sensor_links`.`sensorAddress`\r\nWHERE $__timeFilter(`insertTime`)", + "refId": "Temperature", + "select": [ + [ + { + "params": [ + "id" + ], + "type": "column" + } + ] + ], + "table": "sensor_data", + "timeColumn": "timestamp", + "timeColumnType": "int", + "where": [ + { + "name": "$__unixEpochFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Temperature", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "Time" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": { + "Cactus Large": 2, + "Cactus Small": 4, + "Palm Tree": 3, + "Pineapple": 1, + "Time": 0 + }, + "renameByName": {} + } + } + ], + "transparent": true, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:66", + "decimals": 1, + "format": "celsius", + "label": "", + "logBase": 1, + "max": "55", + "min": "20", + "show": true + }, + { + "$$hashKey": "object:67", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "MySQL - Plant System", + "description": "", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 3, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.4", + "pointradius": 2, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "format": "time_series", + "group": [], + "metricColumn": "sensorID", + "rawQuery": true, + "rawSql": "SELECT\r\n UNIX_TIMESTAMP(`insertTime`) AS `time`,\r\n `sensor_links`.`plantName` AS metric,\r\n (4095-`sensor_data`.`M`) AS `value`\r\nFROM `sensor_data`\r\nLEFT JOIN `sensor_links` ON\r\n`sensor_data`.`address` = `sensor_links`.`sensorAddress`\r\nWHERE $__timeFilter(`insertTime`)", + "refId": "Soil Moisture", + "select": [ + [ + { + "params": [ + "id" + ], + "type": "column" + } + ] + ], + "table": "sensor_data", + "timeColumn": "timestamp", + "timeColumnType": "int", + "where": [ + { + "name": "$__unixEpochFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Soil Moisture", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "Time" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": { + "Cactus Large": 2, + "Cactus Small": 4, + "Palm Tree": 3, + "Pineapple": 1, + "Time": 0 + }, + "renameByName": {} + } + } + ], + "transparent": true, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:66", + "decimals": 0, + "format": "none", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:67", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "MySQL - Plant System", + "description": "", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 21 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.4", + "pointradius": 2, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "format": "time_series", + "group": [], + "metricColumn": "sensorID", + "rawQuery": true, + "rawSql": "SELECT\r\n UNIX_TIMESTAMP(`insertTime`) AS `time`,\r\n `sensor_links`.`plantName` AS metric,\r\n (4095-`sensor_data`.`A`) AS `value`\r\nFROM `sensor_data`\r\nLEFT JOIN `sensor_links` ON\r\n`sensor_data`.`address` = `sensor_links`.`sensorAddress`\r\nWHERE $__timeFilter(`insertTime`)", + "refId": "Soil Moisture", + "select": [ + [ + { + "params": [ + "id" + ], + "type": "column" + } + ] + ], + "table": "sensor_data", + "timeColumn": "timestamp", + "timeColumnType": "int", + "where": [ + { + "name": "$__unixEpochFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Ambient Light", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "Time" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": { + "Cactus Large": 2, + "Cactus Small": 4, + "Palm Tree": 3, + "Pineapple": 1, + "Time": 0 + }, + "renameByName": {} + } + } + ], + "transparent": true, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:66", + "decimals": 0, + "format": "none", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:67", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "MySQL - Plant System", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 31 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 2, + "nullPointMode": "null", + "options": { + "alertThreshold": false + }, + "percentage": false, + "pluginVersion": "7.5.4", + "pointradius": 5, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "format": "time_series", + "group": [], + "metricColumn": "none", + "rawQuery": true, + "rawSql": "SELECT\r\n UNIX_TIMESTAMP(`timestamp`) AS `time`,\r\n `sensor_links`.`plantName` AS metric,\r\n `waterVolume` AS `value`\r\nFROM `watering_data`\r\nLEFT JOIN `sensor_links` ON\r\n`watering_data`.`solenoidAddress` = `sensor_links`.`solenoidAddress`\r\nWHERE $__timeFilter(`timestamp`)", + "refId": "A", + "select": [ + [ + { + "params": [ + "waterVolume" + ], + "type": "column" + } + ] + ], + "table": "watering_data", + "timeColumn": "timestamp", + "timeColumnType": "datetime", + "where": [ + { + "name": "$__timeFilter", + "params": [], + "type": "macro" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Watering data", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transparent": true, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:756", + "format": "mlitre", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:757", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "1m", + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "description": null, + "error": null, + "hide": 2, + "label": "Sensor_0x10", + "name": "Sensor_0x10", + "query": "Cactus", + "skipUrlSync": false, + "type": "constant" + }, + { + "description": null, + "error": null, + "hide": 2, + "label": "Sensor_0x11", + "name": "Sensor_0x11", + "query": "Palm Tree", + "skipUrlSync": false, + "type": "constant" + }, + { + "description": null, + "error": null, + "hide": 2, + "label": "Sensor_0x12", + "name": "Sensor_0x12", + "query": "Avocado Tree", + "skipUrlSync": false, + "type": "constant" + }, + { + "description": null, + "error": null, + "hide": 2, + "label": "Sensor_0x13", + "name": "Sensor_0x13", + "query": "Rose", + "skipUrlSync": false, + "type": "constant" + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Plant Overview", + "uid": "VCPJlvuGz", + "version": 33 +} \ No newline at end of file diff --git a/Part3_Web_and_Automation/mysql/plantsystem.sql b/Part3_Web_and_Automation/mysql/plantsystem.sql new file mode 100644 index 0000000..7f8ff4d --- /dev/null +++ b/Part3_Web_and_Automation/mysql/plantsystem.sql @@ -0,0 +1,112 @@ +-- phpMyAdmin SQL Dump +-- version 5.1.0 +-- https://www.phpmyadmin.net/ +-- +-- Host: 192.168.0.25:3306 +-- Generation Time: Oct 22, 2021 at 02:07 AM +-- Server version: 8.0.24 +-- PHP Version: 7.4.16 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Database: `plantsystem` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `sensor_data` +-- + +CREATE TABLE `sensor_data` ( + `id` int NOT NULL, + `insertTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `address` tinyint UNSIGNED NOT NULL, + `T` float UNSIGNED NOT NULL, + `M` smallint UNSIGNED NOT NULL, + `A` smallint UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Sensor data'; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `sensor_links` +-- + +CREATE TABLE `sensor_links` ( + `id` int UNSIGNED NOT NULL, + `plantName` varchar(25) NOT NULL COMMENT 'Plant name', + `sensorAddress` tinyint UNSIGNED NOT NULL COMMENT 'I2C address of sensor attached to this plant', + `solenoidAddress` tinyint UNSIGNED NOT NULL COMMENT 'I2C address of solenoid attached to this plant' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `watering_data` +-- + +CREATE TABLE `watering_data` ( + `id` int UNSIGNED NOT NULL, + `solenoidAddress` tinyint UNSIGNED NOT NULL, + `waterVolume` smallint UNSIGNED NOT NULL, + `timestamp` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- +-- Indexes for dumped tables +-- + +-- +-- Indexes for table `sensor_data` +-- +ALTER TABLE `sensor_data` + ADD PRIMARY KEY (`id`); + +-- +-- Indexes for table `sensor_links` +-- +ALTER TABLE `sensor_links` + ADD PRIMARY KEY (`id`); + +-- +-- Indexes for table `watering_data` +-- +ALTER TABLE `watering_data` + ADD PRIMARY KEY (`id`); + +-- +-- AUTO_INCREMENT for dumped tables +-- + +-- +-- AUTO_INCREMENT for table `sensor_data` +-- +ALTER TABLE `sensor_data` + MODIFY `id` int NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `sensor_links` +-- +ALTER TABLE `sensor_links` + MODIFY `id` int UNSIGNED NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `watering_data` +-- +ALTER TABLE `watering_data` + MODIFY `id` int UNSIGNED NOT NULL AUTO_INCREMENT; +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/Part3_Web_and_Automation/n8n/PlantSystem_-_Periodic_Watering.json b/Part3_Web_and_Automation/n8n/PlantSystem_-_Periodic_Watering.json new file mode 100644 index 0000000..8581abd --- /dev/null +++ b/Part3_Web_and_Automation/n8n/PlantSystem_-_Periodic_Watering.json @@ -0,0 +1,319 @@ +{ + "name": "Watering System - Periodic Watering", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.start", + "typeVersion": 1, + "position": [ + 460, + 770 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "SELECT * FROM `sensor_links` WHERE `solenoidAddress` <> '0' ORDER BY `solenoidAddress` ASC" + }, + "name": "Select Solenoids", + "type": "n8n-nodes-base.mySql", + "typeVersion": 1, + "position": [ + 660, + 570 + ], + "credentials": { + "mySql": "NAS - MySQL" + } + }, + { + "parameters": { + "batchSize": 1, + "options": { + "reset": false + } + }, + "name": "For Each Solenoid", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 1, + "position": [ + 850, + 570 + ], + "alwaysOutputData": true, + "continueOnFail": true + }, + { + "parameters": { + "url": "http://192.168.0.19/waterPlant", + "responseFormat": "string", + "options": {}, + "queryParametersUi": { + "parameter": [ + { + "name": "address", + "value": "={{$json[\"solenoidAddress\"]}}" + }, + { + "name": "volume", + "value": "={{$json[\"waterVolume\"]}}" + } + ] + } + }, + "name": "Water Plant", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "position": [ + 1260, + 570 + ], + "notesInFlow": false, + "notes": "Send request to water the plant with 100ml of water" + }, + { + "parameters": { + "table": "watering_data", + "columns": "solenoidAddress, waterVolume", + "options": {} + }, + "name": "Update `watering_data`", + "type": "n8n-nodes-base.mySql", + "typeVersion": 1, + "position": [ + 1450, + 770 + ], + "credentials": { + "mySql": "NAS - MySQL" + } + }, + { + "parameters": { + "functionCode": "const sleepTime = 25000; // in miliseconds\n\nfunction sleep(milliseconds) {\n return new Promise(\n resolve => setTimeout(resolve, milliseconds)\n );\n}\n// Sleep\nawait sleep(sleepTime );\n// Output data\nreturn items;\n" + }, + "name": "Delay", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 1450, + 570 + ], + "notesInFlow": false, + "notes": "Sleep" + }, + { + "parameters": { + "conditions": { + "number": [], + "boolean": [ + { + "value1": "={{$node[\"For Each Solenoid\"].context[\"noItemsLeft\"]}}", + "value2": true + } + ] + } + }, + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 1660, + 320 + ] + }, + { + "parameters": {}, + "name": "NoOp", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1860, + 420 + ] + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "waterVolume", + "value": 50 + } + ] + }, + "options": {} + }, + "name": "Water Volume", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [ + 1060, + 570 + ] + }, + { + "parameters": { + "chatId": "1254950103", + "text": "=Finished watering the plants!", + "additionalFields": {} + }, + "name": "Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 1860, + 270 + ], + "credentials": { + "telegramApi": "Telegram N8N" + } + }, + { + "parameters": { + "triggerTimes": { + "item": [ + { + "hour": 5 + }, + { + "hour": 19 + } + ] + } + }, + "name": "Cron", + "type": "n8n-nodes-base.cron", + "typeVersion": 1, + "position": [ + 460, + 570 + ] + }, + { + "parameters": { + "path": "f7048627-a726-4344-8d49-e7ad19c4415a", + "options": {} + }, + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [ + 460, + 370 + ], + "webhookId": "f7048627-a726-4344-8d49-e7ad19c4415a" + } + ], + "connections": { + "Select Solenoids": { + "main": [ + [ + { + "node": "For Each Solenoid", + "type": "main", + "index": 0 + } + ] + ] + }, + "For Each Solenoid": { + "main": [ + [ + { + "node": "Water Volume", + "type": "main", + "index": 0 + } + ] + ] + }, + "Water Plant": { + "main": [ + [ + { + "node": "Delay", + "type": "main", + "index": 0 + }, + { + "node": "Update `watering_data`", + "type": "main", + "index": 0 + } + ] + ] + }, + "Delay": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "NoOp", + "type": "main", + "index": 0 + }, + { + "node": "Telegram", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "For Each Solenoid", + "type": "main", + "index": 0 + } + ] + ] + }, + "Water Volume": { + "main": [ + [ + { + "node": "Water Plant", + "type": "main", + "index": 0 + } + ] + ] + }, + "Cron": { + "main": [ + [ + { + "node": "Select Solenoids", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Select Solenoids", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "id": "8" +} \ No newline at end of file diff --git a/Part3_Web_and_Automation/n8n/PlantSystem_-_Read_Sensor_Data.json b/Part3_Web_and_Automation/n8n/PlantSystem_-_Read_Sensor_Data.json new file mode 100644 index 0000000..ba752fc --- /dev/null +++ b/Part3_Web_and_Automation/n8n/PlantSystem_-_Read_Sensor_Data.json @@ -0,0 +1,186 @@ +{ + "name": "Plant System - Read Sensor Data", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.start", + "typeVersion": 1, + "position": [ + 250, + 350 + ] + }, + { + "parameters": { + "url": "http://192.168.0.19/sensorData", + "allowUnauthorizedCerts": true, + "options": { + "followRedirect": true, + "ignoreResponseCode": true + } + }, + "name": "HTTP Request", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "position": [ + 500, + 350 + ], + "continueOnFail": true + }, + { + "parameters": { + "chatId": "1254950103", + "text": "=HousePlantSystem: Reading {{$node[\"HTTP Request\"].json[\"count\"]}} sensors instead of 4.", + "additionalFields": {} + }, + "name": "Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 1000, + 150 + ], + "credentials": { + "telegramApi": "Telegram N8N" + } + }, + { + "parameters": { + "table": "sensor_data", + "columns": "address, T, M, A", + "options": {} + }, + "name": "MySQL", + "type": "n8n-nodes-base.mySql", + "typeVersion": 1, + "position": [ + 1000, + 350 + ], + "credentials": { + "mySql": "NAS - MySQL" + } + }, + { + "parameters": { + "functionCode": "const sensorData = [];\n\nfor (let i=0; i setTimeout(resolve, milliseconds)\n );\n}\n// Sleep\nawait sleep(sleepTime );\n// Output data\nreturn items;\n" + }, + "name": "Delay", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 1700, + 550 + ], + "notesInFlow": false, + "notes": "Sleep" + }, + { + "parameters": { + "conditions": { + "number": [], + "boolean": [ + { + "value1": "={{$node[\"For Each Solenoid\"].context[\"noItemsLeft\"]}}", + "value2": true + } + ] + } + }, + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 1900, + 300 + ] + }, + { + "parameters": {}, + "name": "NoOp", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 2100, + 300 + ] + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "waterVolume", + "value": 50 + } + ] + }, + "options": {} + }, + "name": "Water Volume", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [ + 1300, + 550 + ] + }, + { + "parameters": { + "chatId": "1254950103", + "text": "=Watering {{$json[\"plantName\"]}} with {{$json[\"waterVolume\"]}}mL of water", + "additionalFields": {} + }, + "name": "Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 1500, + 950 + ], + "credentials": { + "telegramApi": "Telegram N8N" + } + }, + { + "parameters": { + "path": "4d08ae89-8eba-4ee4-80c2-d8f93ff51adb", + "options": {} + }, + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [ + 700, + 390 + ], + "webhookId": "4d08ae89-8eba-4ee4-80c2-d8f93ff51adb" + } + ], + "connections": { + "Start": { + "main": [ + [ + { + "node": "Select Solenoids", + "type": "main", + "index": 0 + } + ] + ] + }, + "Select Solenoids": { + "main": [ + [ + { + "node": "For Each Solenoid", + "type": "main", + "index": 0 + } + ] + ] + }, + "For Each Solenoid": { + "main": [ + [ + { + "node": "Water Volume", + "type": "main", + "index": 0 + } + ] + ] + }, + "Delay": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Water Plant": { + "main": [ + [ + { + "node": "Delay", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "NoOp", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "For Each Solenoid", + "type": "main", + "index": 0 + } + ] + ] + }, + "Water Volume": { + "main": [ + [ + { + "node": "Update `watering_data`", + "type": "main", + "index": 0 + }, + { + "node": "Telegram", + "type": "main", + "index": 0 + }, + { + "node": "Water Plant", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Select Solenoids", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": {}, + "id": "4" +} \ No newline at end of file diff --git a/Part3_Web_and_Automation/n8n/PlantSystem_-_Water_Specific_Plant.json b/Part3_Web_and_Automation/n8n/PlantSystem_-_Water_Specific_Plant.json new file mode 100644 index 0000000..2854a89 --- /dev/null +++ b/Part3_Web_and_Automation/n8n/PlantSystem_-_Water_Specific_Plant.json @@ -0,0 +1,208 @@ +{ + "name": "PlantSystem - Water Specific Plant", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.start", + "typeVersion": 1, + "position": [ + 800, + 750 + ] + }, + { + "parameters": { + "url": "http://192.168.0.19/waterPlant", + "responseFormat": "string", + "options": {}, + "queryParametersUi": { + "parameter": [ + { + "name": "address", + "value": "={{$json[\"solenoidAddress\"]}}" + }, + { + "name": "volume", + "value": "={{$node[\"Webhook\"].json[\"query\"][\"volume\"]}}" + } + ] + } + }, + "name": "Water Plant", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "position": [ + 1200, + 550 + ], + "notesInFlow": false, + "alwaysOutputData": true, + "continueOnFail": true, + "notes": "Send request to water the plant with 100ml of water" + }, + { + "parameters": { + "path": "756fac76-f3d7-4fce-b586-27e114afba46", + "options": {} + }, + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [ + 800, + 550 + ], + "webhookId": "756fac76-f3d7-4fce-b586-27e114afba46" + }, + { + "parameters": { + "operation": "executeQuery", + "query": "=SELECT * from `sensor_links` where `solenoidAddress` = '{{$json[\"query\"][\"solenoidAddress\"]}}' LIMIT 1" + }, + "name": "MySQL", + "type": "n8n-nodes-base.mySql", + "typeVersion": 1, + "position": [ + 1000, + 550 + ], + "credentials": { + "mySql": "NAS - MySQL" + } + }, + { + "parameters": { + "table": "watering_data", + "columns": "=solenoidAddress, waterVolume", + "options": {} + }, + "name": "Insert Water Amount", + "type": "n8n-nodes-base.mySql", + "typeVersion": 1, + "position": [ + 1810, + 450 + ], + "credentials": { + "mySql": "NAS - MySQL" + } + }, + { + "parameters": { + "functionCode": "items[0].json.waterVolume = $node[\"Webhook\"].json[\"query\"][\"volume\"];\nitems[0].json.solenoidAddress= $node[\"MySQL\"].json[\"solenoidAddress\"];\nreturn items;\n" + }, + "name": "Function", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 1610, + 450 + ] + }, + { + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{$json[\"data\"]}}", + "operation": "contains", + "value2": "\"result\": OK" + } + ] + } + }, + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 1400, + 550 + ] + }, + { + "parameters": { + "chatId": "1254950103", + "text": "=Water specific plant failed: Plant #{{$node[\"MySQL\"].json[\"sensorAddress\"]}} - Volume:{{$node[\"Webhook\"].json[\"query\"][\"volume\"]}}ml", + "additionalFields": {} + }, + "name": "Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 1600, + 650 + ], + "credentials": { + "telegramApi": "Telegram N8N" + } + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "MySQL", + "type": "main", + "index": 0 + } + ] + ] + }, + "MySQL": { + "main": [ + [ + { + "node": "Water Plant", + "type": "main", + "index": 0 + } + ] + ] + }, + "Function": { + "main": [ + [ + { + "node": "Insert Water Amount", + "type": "main", + "index": 0 + } + ] + ] + }, + "Water Plant": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "Function", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Telegram", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": {}, + "id": "5" +} \ No newline at end of file diff --git a/README.md b/README.md index 622f631..2ce6991 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ Blog page: https://sasakaranovic.com/projects/diy-house-plants-watering-system-p Build watering system for house plants. In part two of this video series, we will create a central unit that takes sensor readings and also waters our plants on command. -## Part 3: Dashboard +## Part 3: Automation and Conclusion + +[![DIY House Plants Watering System - Part 3: Automation and Conclusion](http://i3.ytimg.com/vi/jLdTUMHOhRE/maxresdefault.jpg)](https://youtu.be/jLdTUMHOhRE) + +Watch: [DIY House Plants Watering System // Part 2: Automation and Conclusion](https://youtu.be/jLdTUMHOhRE) + +Blog page: https://sasakaranovic.com/projects/diy-house-plants-watering-system-part-3-automation/ + +Build watering system for house plants. In part two of this video series, we will create a central unit that takes sensor readings and also waters our plants on command. -TBD