The Worg Project

The Worg project is an embedded system for environmental monitoring of a greenhouse with 4 plants. By reading soil moisture and environmental conditions, the system controls humidity levels, temperature, VPD (Vapour-pressure Deficit), and the automatic watering of the plants.
The architecture is based on an ESP32 programmed in MicroPython, communication via MQTT protocol with Mosquitto Broker, data persistence in InfluxDB, and custom visualization in Grafana, all integrated in this WordPress site for project presentation.
All data below is updated in real time every 10 minutes.

Real-Time Metrics

Environment Conditions:

Electrical data:

Soil Moisture (In development):

Ecosystem

Using an ESP32 microcontroller, environmental data such as temperature, humidity, pressure, and soil moisture are collected via different protocols (I2C, Modbus) and analog signals. Based on this information, the system performs automated controls, such as activating fans, humidifiers, and irrigation pumps, as well as managing lighting according to the plant’s growth stage. All data is sent via MQTT protocol to a Mosquitto broker hosted on a VPS, where it is stored in the InfluxDB database through Telegraf. Finally, the information is visualized in real time on Grafana, with visual charts displayed on WordPress, also hosted in the same virtualized environment using Docker.

For the development of the application, a set of technologies that communicate with each other and feature collaborative and open-source development was chosen. Among them, I highlight the solutions below, along with their respective configuration files, to provide an understanding of the project’s operational behavior.

‘The MQTT protocol provides a lightweight method of carrying out messaging using a publish/subscribe model. This makes it suitable for Internet of Things messaging such as with low power sensors or mobile devices such as phones, embedded computers or microcontrollers.’

Website: https://mosquitto.org

System configuration file:

# Basic configuration listener 1883 0.0.0.0 allow_anonymous false password_file /etc/mosquitto/passwd # TLS listener with certificate settings #listener 8883 0.0.0.0 #cafile /etc/mosquitto/certs/ca.crt #certfile /etc/mosquitto/certs/server.crt #keyfile /etc/mosquitto/certs/server.key #tls_version tlsv1.2 #allow_anonymous false # Other settings persistence true persistence_location /var/lib/mosquitto/ log_dest file /var/log/mosquitto/mosquitto.log autosave_interval 1800 include_dir /etc/mosquitto/conf.d # Performance settings max_keepalive 300 persistent_client_expiration 7d connection_messages true retry_interval 20 max_inflight_messages 100 max_queued_messages 1000 message_size_limit 0 set_tcp_nodelay true

Details:

  • Due to the ESP32 not having enough processing power to run all the programmed functions, it was not possible to establish an SSL connection between the device and the broker. The communication worked, but the control and IoT sending codes were impaired. This will be adjusted as described in ‘Next Steps’.
  • Although the broker settings are configured with a ‘Keep Alive’ of 300 seconds and messages are sent every 10 minutes, the connection could be renewed whenever necessary. If more processes accumulate in the future, an adjustment might be needed.

‘Telegraf is an open source plugin-driven server agent for collecting and reporting time series data. Written in Go and compiled as a standalone binary, you can execute it on any system with no external dependencies. Telegraf also contains in-memory metric buffers to maintain data collection if the downstream database is temporarily unavailable.’

Website: https://www.influxdata.com/time-series-platform/telegraf

System configuration file (Sensitive data has been replaced with ‘###’):

[agent] interval = “10s” round_interval = true metric_batch_size = 1000 metric_buffer_limit = 10000 collection_jitter = “0s” flush_interval = “10s” flush_jitter = “0s” precision = “” [[inputs.mqtt_consumer]] servers = [“tcp://localhost:1883”] topics = [“worg/#”] data_format = “json” username = “###” password = “###” [[outputs.influxdb_v2]] urls = [“http://localhost:8086”] token = “###” organization = “worg” bucket = “worg_bucket” #[[outputs.loki]] # url = “http://localhost:3100/loki/api/v1/push” # timeout = “10s” # [outputs.loki.labels] # job = “telegraf” # host = “srv1085262” # topic = “mqtt_metrics” # test: telegraf –config telegraf.conf –test

Details:

  • I chose to load all MQTT message topics, as data collection is done only for this project, so it is configured as: topics = ['worg/#'].
  • Port 1883 refers to the broker, installed on the same VPS.
  • Port 8086 refers to the InfluxDB database, on the same VPS.
  • There is an output configuration for the ‘Loki’ service, included in ‘Next Steps’.

‘InfluxDB is a time series database (TSDB) developed by the company InfluxData. It is used for storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet of Things sensor data, and real-time analytics.’

Website: https://www.influxdata.com

System configuration file: There is none; all configurations were built through the service’s web interface.

Details:

  • Due to the adjustment phase of the data retention period in relation to the available storage on the VPS, it is currently configured for only 1 month. This will be adjusted according to the amount of data sent per day and monitored for capacity efficiency.

‘Easily collect, correlate, and visualize data with beautiful dashboards using Grafana — the open source data visualization and monitoring solution that drives informed decisions, enhances system performance, and streamlines troubleshooting’.

Website: https://grafana.com

System configuration file (Sensitive data has been replaced with ‘###’):

[server] # Protocol (http, https, h2, socket) protocol = http # The ip address to bind to, empty will bind to all interfaces http_addr = 0.0.0.0 # The http port to use http_port = 3000 # The public facing domain name used to access grafana from a browser domain = roni.engineer # Redirect to correct domain if host header does not match domain # Prevents DNS rebinding attacks ;enforce_domain = true # The full public facing url you use in browser, used for redirects and emails # If you use reverse proxy and sub path specify full url (with sub path) root_url = https://roni.engineer:8443/ # Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons. serve_from_sub_path = true [auth.anonymous] # enable anonymous access enabled = true # specify organization name that should be used for unauthenticated users org_name = Main Org. # specify role for unauthenticated users org_role = Viewer [unified_alerting.prometheus_conversion] # Configuration options for converting Prometheus alerting and recording rules to Grafana rules. # These settings affect rules created via the Prometheus conversion API. # Offset the rule evaluation time for imported rules by a specified duration in the past. # This offset is applied and saved to the rule query during the conversion process from Prometheus to Grafana format. # The setting only affects rules imported after the configuration change is made and does not modify existing rules. # Accepts duration formats like: 30s, 1m, 1h. rule_query_offset = 1m #################################### Recording Rules ##################### [recording_rules] # Enable recording rules. enabled = true # Request timeout for recording rule writes. timeout = 30s # Default data source UID to write to if not specified in the rule definition. default_datasource_uid = # Optional custom headers to include in recording rule write requests. [recording_rules.custom_headers] # exampleHeader = exampleValue [feature_highlights] # Setting ‘enabled’ to true enables highlighting Enterprise features in UI. enabled = false

Details:

  • In [auth.anonymous], access status is enabled so that the charts generated via iframe and displayed on the WordPress website are accessible to anyone, but only for viewing.
  • This demonstration of the configuration file is only an excerpt of the adjustments made. All other default example information remains on the server but is not available here, as it is too extensive.

‘A safer container ecosystem, for everyone. Free hardened images give every developer a trusted starting point, with enterprise options for SLAs, compliance, and extended lifecycle security’.

Website: https://www.docker.com

System configuration file (Docker-compose, sensitive data has been replaced with ‘###’):

version: “3.8” services: db: image: mysql:latest restart: always environment: MYSQL_ROOT_PASSWORD: ### MYSQL_DATABASE: ### MYSQL_USER: ### MYSQL_PASSWORD: ### volumes: – “mysql_data:/var/lib/mysql” networks: – internal wordpress: depends_on: – db image: wordpress:latest restart: always expose: – “80” environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: ### WORDPRESS_DB_PASSWORD: ### WORDPRESS_DB_NAME: ### WORDPRESS_CONFIG_EXTRA: | define(‘FORCE_SSL_ADMIN’, true); if (isset($$_SERVER[‘HTTP_X_FORWARDED_PROTO’]) && $$_SERVER[‘HTTP_X_FORWARDED_PROTO’] == ‘https’) { $$_SERVER[‘HTTPS’] = ‘on’; } volumes: – “./wp-content:/var/www/html/wp-content” networks: – internal nginx: image: nginx:alpine restart: always ports: – “80:80” – “443:443” – “8080:8080” – “8443:8443” volumes: – “./nginx.conf:/etc/nginx/nginx.conf:ro” – “/###.pem:ro” – “/###.key:ro” depends_on: – wordpress networks: – internal phpmyadmin: image: phpmyadmin/phpmyadmin restart: always expose: – “80” environment: PMA_HOST: db PMA_USER: ### PMA_PASSWORD: ### depends_on: – db networks: – internal networks: internal: driver: bridge volumes: mysql_data: {}

Details:

  • Only WordPress was built under a container to facilitate the installation and maintenance of the web server and SSL.

‘The open source publishing platform of choice for millions of websites worldwide—from creators and small businesses to enterprises’.

Website: https://wordpress.org

System configuration file: There is none; all configurations were built through the service’s web interface.

Details:

A simple theme with few resources was used. The purpose of this website is merely explanatory within the context of the project as a whole. However, two plugins were used to improve reader usability, namely:

  • Page scroll to id, so that the website resembled a single continuous page with anchors in the menus.
  • Sticky Menu & Sticky Header, so that the menu scrolled along with the text during the reading experience.
  • WP Image Zoom, so that the electronic diagram could be visualized with more details.

The Code

The system was developed in MicroPython running on an ESP32, in which all the libraries used, the main code, and other files are demonstrated in the following topics. Part of the development was set aside to be executed in the future, as seen in ‘Next Steps’, due to difficulties encountered during development, including electronic problems and issues with process concurrency, which is limited to two threads on this microcontroller.

The complete repository can be accessed at the link: https://github.com/roninanini/worg

Libraries Used

Below, are all the libraries used for the development of the application, with proper credits and a brief explanation of how they were incorporated into the main code.

bme280.py

This library was used to collect temperature, humidity, and environment pressure data from a BME280 sensor with I2C communication.

ds3231.py

For time control, an RTC module was used so that it would not depend on internet connection or be affected by microcontroller reboots. This way, this module with an internal battery can manage dates and times offline.

mcp23017.py

Due to the number of devices connected between inputs and outputs, it was necessary to use an MCP23017 module to expand these ports. As a result, there are still spare ports that can be used in future adjustments.

pzem.py

To collect the project’s electrical data, such as voltage, current, frequency, active power, power factor, and active energy, a PZEM-004T module with Modbus-RTU communication was used.

umqtt_simple.py

For MQTT communication between the ESP32 and the Mosquitto broker, this simple yet functional library was used.

soil.py

from machine import Pin, ADC from time import sleep import time import _thread import gc class Soil: def __init__(self): # ATRIBUTES self.PLANT_1 = ADC(Pin(34)) self.PLANT_2 = ADC(Pin(35)) self.PLANT_3 = ADC(Pin(32)) self.PLANT_4 = ADC(Pin(33)) self.pack_time = 5*60 #seconds self.read_by_pack = 10 #seconds # configure ADC ports to 0-3.3V (ESP32) self.PLANT_1.atten(ADC.ATTN_11DB) self.PLANT_2.atten(ADC.ATTN_11DB) self.PLANT_3.atten(ADC.ATTN_11DB) self.PLANT_4.atten(ADC.ATTN_11DB) # creating lists to store values self.LIST_PLANT1 = [] self.LIST_PLANT2 = [] self.LIST_PLANT3 = [] self.LIST_PLANT4 = [] # making value 0 to average value self.AVERAGE_PLANT1 = 4095 self.AVERAGE_PLANT2 = 4095 self.AVERAGE_PLANT3 = 4095 self.AVERAGE_PLANT4 = 4095 # defining the initial time self.thread_started = False self.start_time = time.time() def soil_loop(self): self.thread_started = True while True: gc.collect() # reading the value of sensors value_PLANT1 = self.PLANT_1.read() value_PLANT2 = self.PLANT_2.read() value_PLANT3 = self.PLANT_3.read() value_PLANT4 = self.PLANT_4.read() # storing the values in the list self.LIST_PLANT1.append(value_PLANT1) self.LIST_PLANT2.append(value_PLANT2) self.LIST_PLANT3.append(value_PLANT3) self.LIST_PLANT4.append(value_PLANT4) # verifing the time if time.time() – self.start_time >= (self.pack_time): # calculating the average of the reads self.AVERAGE_PLANT1 = sum(self.LIST_PLANT1) / len(self.LIST_PLANT1) self.AVERAGE_PLANT2 = sum(self.LIST_PLANT2) / len(self.LIST_PLANT2) self.AVERAGE_PLANT3 = sum(self.LIST_PLANT3) / len(self.LIST_PLANT3) self.AVERAGE_PLANT4 = sum(self.LIST_PLANT4) / len(self.LIST_PLANT4) # reboot lists and time self.LIST_PLANT1 = [] self.LIST_PLANT2 = [] self.LIST_PLANT3 = [] self.LIST_PLANT4 = [] self.start_time = time.time() # waiting 1 sec to next read sleep(self.read_by_pack) def get_soil(self): return self.AVERAGE_PLANT1, self.AVERAGE_PLANT2, self.AVERAGE_PLANT3, self.AVERAGE_PLANT4 sensor_soil = Soil() _thread.start_new_thread(sensor_soil.soil_loop, ())

Soil moisture readings proved to be extremely unstable in several tests, which could cause incorrect watering of the plants. Therefore, I developed a small script that performs multiple readings over a predetermined time and stores them in lists. After that time, the average value of the measurements is stored in a variable.
Furthermore, it was necessary to use a dedicated thread for this function, so that the collection is constantly performed, regardless of the state of the main code. This ensures that the control logic always remains the priority.

Variables

To facilitate the development of the main code (‘main’), I chose to create a kind of ‘variable library’ for all data reading and control points. This way, it became clearer and easier to identify the microcontroller’s behavior. In this Python file, all variables collected from the electronic module libraries used, the calculations, and the microcontroller’s control points are concentrated.

variables.py

from machine import I2C, Pin, UART import Libs.mcp23017 import Libs.bme280 from Libs.soil import sensor_soil from Libs.pzem import PZEM import math from Libs.ds3231 import DS3231 i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000) mcp = Libs.mcp23017.MCP23017(i2c, 0x27) uart = UART(2, baudrate=9600,timeout=500) dev = PZEM(uart=uart,addr=0x01) bme = Libs.bme280.BME280(i2c=i2c) ds = DS3231(i2c) #Address of mcp: 39 (dec) = 27 (hex) #Address of bme: 118 (dec) = 76 (hex) #Address of ds: 104 (dec) = 68 (hex) class IO: def __init__(self): self.led = Pin(2, Pin.OUT) self.VPD = 0 self.VP = 0 self.VPL = 0 self.water_pump_1_state = 0 self.water_pump_2_state = 0 self.water_pump_3_state = 0 self.water_pump_4_state = 0 self.lighting_state = 0 self.fan_1_state = 0 self.fan_2_state = 0 self.humidifier_state = 0 self.deshumidifier_state = 0 # ————— OUTPUTS —————# def water_pump_1(self, state): mcp.pin(3, mode=0, value=state) self.water_pump_1_state = state # store the state def water_pump_2(self, state): mcp.pin(2, mode=0, value=state) self.water_pump_2_state = state def water_pump_3(self, state): mcp.pin(1, mode=0, value=state) self.water_pump_3_state = state def water_pump_4(self, state): mcp.pin(0, mode=0, value=state) self.water_pump_4_state = state def lighting(self, state): mcp.pin(13, mode=0, value=state) self.lighting_state = state def fan_1(self, state): mcp.pin(15, mode=0, value=state) self.fan_1_state = state def fan_2(self, state): mcp.pin(14, mode=0, value=state) self.fan_2_state = state def humidifier(self, state): mcp.pin(12, mode=0, value=state) self.humidifier_state = state def deshumidifier(self, state): mcp.pin(11, mode=0, value=state) self.deshumidifier_state = state # ————— GETTERS TO STATUS —————# def get_water_pump_1_status(self): return self.water_pump_1_state def get_water_pump_2_status(self): return self.water_pump_2_state def get_water_pump_3_status(self): return self.water_pump_3_state def get_water_pump_4_status(self): return self.water_pump_4_state def get_lighting_status(self): return self.lighting_state def get_fan_1_status(self): return self.fan_1_state def get_fan_2_status(self): return self.fan_2_state def get_humidifier_status(self): return self.humidifier_state def get_deshumidifier_status(self): return self.deshumidifier_state #————— INPUTS —————# def soil_1(self): return sensor_soil.AVERAGE_PLANT1 def soil_2(self): return sensor_soil.AVERAGE_PLANT2 def soil_3(self): return sensor_soil.AVERAGE_PLANT3 def soil_4(self): return sensor_soil.AVERAGE_PLANT4 def temp(self): read_temp, _, _ = bme.read_compensated_data() return read_temp def pressure(self): _, read_pressure, _ = bme.read_compensated_data() return read_pressure / 100 def humid(self): _, _, read_humid = bme.read_compensated_data() return read_humid def vpd(self): # vapor pressure of air (VP): self.VP = 0.61078 * math.exp(17.27 * self.temp() / (self.temp() + 237.03)) * (self.humid()/100) # vapor pressure in the leaf (VPL): self.VPL = 0.61078 * math.exp(17.27 * self.temp() / (self.temp() + 237.03)) # vapor pressure deficit (VPD): self.VPD = self.VPL – self.VP return self.VPD def hour(self, hour=None): #(year, month, mday, wday, hour, minute, second, 0) return ds.datetime() #————— ETC —————# def voltage(self): dev.read() return dev.getVoltage() def current(self): dev.read() return dev.getCurrent() def active_power(self): dev.read() return dev.getActivePower() def active_energy(self): dev.read() return dev.getActiveEnergy() def frequency(self): dev.read() return dev.getFrequency() def power_factor(self): dev.read() return dev.getPowerFactor()

The MAIN code

The main code is divided into two main fronts: environmental control, which is the priority to function under any circumstances, especially if there is no internet connection or if a sensor fails, and MQTT connection. To achieve this, all sensors and generated data considered as ‘inputs’ are read, including ‘plant phase’, which is received via MQTT. Then, the control of plant watering, lighting, fans, and humidity is carried out. In parallel, the system connects to the broker, receives and sends data.

Below, is the basic operation of the code:

main.py

import network from time import sleep import time from variables import IO from Libs.umqtt_simple import MQTTClient from Libs.ds3231 import DS3231 from machine import I2C, Pin, ADC import Libs.mcp23017 from Libs.soil import sensor_soil import passwords # ———–ATRIBUTES———–# io = IO() SSID = passwords.SSID PASSWORD = passwords.PASSWORD MQTT_ID = passwords.MQTT_ID MQTT_SERVER = passwords.MQTT_SERVER MQTT_PORT = passwords.MQTT_PORT MQTT_USER = passwords.MQTT_USER MQTT_PASSWORD = passwords.MQTT_PASSWORD i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000) mcp = Libs.mcp23017.MCP23017(i2c, 0x27) ds = DS3231(i2c) circle = 10*60 # ———–INTERNET CONECTION ———-# def setup_wifi(): wlan = network.WLAN(network.STA_IF) wlan.active(True) print(“Connecting to WiFi…”) print(f”SSID: {SSID}”) if not wlan.isconnected(): wlan.connect(SSID, PASSWORD) print(“Trying connection…”) return wlan # ———–SETTING PARAMETER OF LIGHT AND ENVIRONMENT ———-# def write_data(topic, message): topic = topic.decode(‘utf-8’) value = message.decode(‘utf-8’) plant_phase = read_data() if topic == ‘worg/plant_phase’: plant_phase = value with open(‘data.csv’, ‘w’) as f: f.write(f'{plant_phase}’) def read_data(): try: with open(‘data.csv’, ‘r’) as f: line = f.read() return line.strip() except Exception as e: return “0” #SETTING CONDITIONS TO CONTROL def water_plant(soil_moisture, state, water_pump, plant_name=””): “””Water a plant based on soil moisture and state””” if soil_moisture < 10: print(f"Watering {plant_name} - State: {state}") # Determine watering cycles based on state if state == 0: cycles = 0 elif state == 1: cycles = 3 elif state == 2: cycles = 6 elif state == 3: cycles = 12 else: cycles = 0 # Execute watering cycles while cycles > 0: water_pump(1) # Turn pump ON sleep(10) # Water for 10 seconds water_pump(0) # Turn pump OFF sleep(10) # Pause for 10 seconds cycles -= 1 # Decrement counter wlan = None try: wlan = setup_wifi() except Exception as e: pass #MQTT CONNECT client_mqtt = MQTTClient(MQTT_ID, server=MQTT_SERVER, port=MQTT_PORT, user=MQTT_USER, password=MQTT_PASSWORD, keepalive=300) client_mqtt.set_callback(write_data) sleep(10) while True: if wlan is None: try: wlan = setup_wifi() except: sleep(5) continue if not wlan.isconnected(): print(“WiFi disconnected, trying connection…”) wlan = setup_wifi() timeout = 20 start = time.time() while not wlan.isconnected() and time.time() – start < timeout: sleep(1) if wlan.isconnected(): try: client_mqtt.connect() client_mqtt.check_msg() client_mqtt.subscribe('worg/plant_phase', qos=1) except Exception as e: print("MQTT reconnection error") pass if wlan.isconnected(): try: try: temp = io.temp() humid = io.humid() pressure = io.pressure() vpd = io.vpd() except Exception as e: temp = 25 humid = 60 pressure = 950 vpd = 1 try: voltage = io.voltage() current_val = io.current() active_power = io.active_power() active_energy = io.active_energy() frequency = io.frequency() power_factor = io.power_factor() except Exception as e: voltage = 120 current_val = 2 active_power = 1 active_energy = 1 frequency = 60 power_factor = 1 # SENSORES DE SOLO try: soil_1 = str(io.soil_1()) except Exception as e: soil_1 = '4095' try: soil_2 = str(io.soil_2()) except Exception as e: soil_2 = '4095' try: soil_3 = str(io.soil_3()) except Exception as e: soil_3 = '4095' try: soil_4 = str(io.soil_4()) except Exception as e: soil_4 = '4095' # -----------POINTS OF WEATHER TO GRAFANA -----------# client_mqtt.publish('worg/weather/temp', f'{{"temperature": {temp}}}', retain=False, qos=1) client_mqtt.publish('worg/weather/humid', f'{{"humidity": {humid}}}', retain=False, qos=1) client_mqtt.publish('worg/weather/pressure', f'{{"pressure": {pressure}}}', retain=False, qos=1) client_mqtt.publish('worg/weather/vpd', f'{{"Vapour-pressure deficit": {vpd}}}', retain=False, qos=1) # -----------ELECTRICAL POINTS TO GRAFANA -----------# client_mqtt.publish('worg/electrical/voltage', f'{{"voltage": {voltage}}}', retain=False, qos=1) client_mqtt.publish('worg/electrical/current', f'{{"current": {current_val}}}', retain=False, qos=1) client_mqtt.publish('worg/electrical/active_power', f'{{"active power": {active_power}}}', retain=False, qos=1) client_mqtt.publish('worg/electrical/active_energy', f'{{"active energy": {active_energy}}}', retain=False, qos=1) client_mqtt.publish('worg/electrical/frequency', f'{{"frequency": {frequency}}}', retain=False, qos=1) client_mqtt.publish('worg/electrical/power_factor', f'{{"power factor": {power_factor}}}', retain=False, qos=1) # -----------POINTS OF SOIL TO GRAFANA -----------# client_mqtt.publish('worg/soil1', f'{{"soil_moisture 1": {soil_1}}}', retain=False, qos=1) client_mqtt.publish('worg/soil2', f'{{"soil_moisture 2": {soil_2}}}', retain=False, qos=1) client_mqtt.publish('worg/soil3', f'{{"soil_moisture 3": {soil_3}}}', retain=False, qos=1) client_mqtt.publish('worg/soil4', f'{{"soil_moisture 4": {soil_4}}}', retain=False, qos=1) print('mqtt enviado...') except Exception as e: try: client_mqtt.disconnect() except: pass try: # CONTROL if io.temp() < 18: # Too cold io.fan_1(0) io.fan_2(0) try: client_mqtt.publish('worg/status/fan_1', f'{{"Fan 01": 0}}', retain=True,qos=1) client_mqtt.publish('worg/status/fan_2', f'{{"Fan 02": 0}}', retain=True,qos=1) except Exception as e: pass elif 18 <= io.temp() < 22: # Slightly warm - minimal cooling io.fan_1(1) io.fan_2(0) try: client_mqtt.publish('worg/status/fan_1', f'{{"Fan 01": 1}}', retain=True,qos=1) client_mqtt.publish('worg/status/fan_2', f'{{"Fan 02": 0}}', retain=True,qos=1) except Exception as e: pass elif 22 <= io.temp() < 25: # Moderately warm - more cooling io.fan_1(1) io.fan_2(1) try: client_mqtt.publish('worg/status/fan_1', f'{{"Fan 01": 1}}', retain=True,qos=1) client_mqtt.publish('worg/status/fan_2', f'{{"Fan 02": 1}}', retain=True,qos=1) except Exception as e: pass else: # Too hot - maximum cooling io.fan_1(1) io.fan_2(1) try: client_mqtt.publish('worg/status/fan_1', f'{{"Fan 01": 1}}', retain=True, qos=1) client_mqtt.publish('worg/status/fan_2', f'{{"Fan 02": 1}}', retain=True, qos=1) except Exception as e: pass states = read_data() if int(states[0]) == 1: vpd_min = 0.5 vpd_max = 0.7 hour_min = 10 hour_max = 16 elif int(states[0]) == 2: vpd_min = 0.7 vpd_max = 1 hour_min = 10 hour_max = 16 elif int(states[0]) == 3: vpd_min = 1 vpd_max = 1.3 hour_min = 7 hour_max = 19 else: vpd_min = 0.8 vpd_max = 1.2 hour_min = 10 hour_max = 16 if io.vpd() < vpd_min: io.deshumidifier(1) io.humidifier(0) try: client_mqtt.publish('worg/status/deshumidifier', f'{{"Deshumidifier": 1}}', retain=True, qos=1) client_mqtt.publish('worg/status/humidifier', f'{{"Humidifier": 0}}',retain=True, qos=1) except Exception as e: pass elif vpd_min <= io.vpd() < vpd_max: io.deshumidifier(0) io.humidifier(0) try: client_mqtt.publish('worg/status/deshumidifier', f'{{"Deshumidifier": 0}}', retain=True, qos=1) client_mqtt.publish('worg/status/humidifier', f'{{"Humidifier": 0}}', retain=True, qos=1) except Exception as e: pass else: io.deshumidifier(0) io.humidifier(1) try: client_mqtt.publish('worg/status/deshumidifier', f'{{"Deshumidifier": 0}}', retain=True, qos=1) client_mqtt.publish('worg/status/humidifier', f'{{"Humidifier": 1}}', retain=True, qos=1) except Exception as e: pass # Light control HOUR = ds.hour() if hour_min <= HOUR < hour_max: io.lighting(0) try: client_mqtt.publish('worg/status/lighting', f'{{"Lighting": 0}}',retain=True, qos=1) except Exception as e: pass else: io.lighting(1) try: client_mqtt.publish('worg/status/lighting', f'{{"Lighting": 1}}',retain=True, qos=1) except Exception as e: pass #CONTROLLING PLANTS WATERING states = read_data() water_plant(io.soil_1(), int(states[0]), io.water_pump_1, "Plant 1") try: client_mqtt.publish('worg/status/water_pump1', f'{{"Water Pump 01": {"1" if mcp.pin(3) else "0"}}}',retain=True, qos=1) except Exception as e: pass water_plant(io.soil_2(), int(states[0]), io.water_pump_2, "Plant 2") try: client_mqtt.publish('worg/status/water_pump2', f'{{"Water Pump 02": {"1" if mcp.pin(2) else "0"}}}',retain=True, qos=1) except Exception as e: pass water_plant(io.soil_3(), int(states[0]), io.water_pump_3, "Plant 3") try: client_mqtt.publish('worg/status/water_pump3', f'{{"Water Pump 03": {"1" if mcp.pin(1) else "0"}}}',retain=True, qos=1) except Exception as e: pass water_plant(io.soil_4(), int(states[0]), io.water_pump_4, "Plant 4") try: client_mqtt.publish('worg/status/water_pump4', f'{{"Water Pump 04": {"1" if mcp.pin(0) else "0"}}}',retain=True, qos=1) except Exception as e: pass print("Leitura media: ", sensor_soil.AVERAGE_PLANT1, sensor_soil.AVERAGE_PLANT2, sensor_soil.AVERAGE_PLANT3, sensor_soil.AVERAGE_PLANT4) except Exception as e: pass sleep(circle)

Details:

  • Sensitive data such as usernames and passwords were stored in a separate file, ‘passwords.py’, which is not synced with GitHub. However, in the directory there is a file called ‘passwords_example.py’ for demonstration purposes.
  • Plant watering is conditioned to cycles according to growth phase. For every second the water pump is on, 10ml is dispensed into the soil. That is, during the vegetation phase, 6 cycles of 100ml are carried out, with a 10-second wait for soil absorption until the next cycle.
  • The control of ‘plant phase’, which is important for watering, light, and humidity control, is still done by sending an MQTT message that writes to this file, with the following values: 0 – none, 1 – primary vegetation, 2 – vegetation, and 3 – flowering. Manipulating this file in another way, perhaps with a graphical interface on this same website, has been added to ‘Next Steps’.
  • MQTT sending is not a priority; environmental control is. However, there are several reconnection attempts in case of failure.
  • Some data is forced into variables in case of sensor reading failure. Log monitoring through Grafana Loki has been added to ‘Next Steps’.
  • The VPD calculation is based on the plant’s phase. For example, in the early cycles, the plant needs a low value, and in more advanced phases, it needs a higher value. This value can be checked in the table below, and a better understanding can be accessed through the provided link.

For understanding: https://www.omnicalculator.com/biology/vapor-pressure-deficit

Physical Build

For all the project’s functionalities to work, several modules and components were used, interconnected directly to the ESP32, which receives inputs, processes the information, and triggers outputs. Among the modules used, there was a range of signal acquisition methods, such as analog signals from the soil moisture transmitters, I2C communication to connect to the real-time clock module (DS3231), the I/O expansion module (MCP23017), and the environment temperature and humidity sensor (BME280), activating the control relays, as well as Modbus RTU communication to capture electrical data.

Below, is how the project was assembled:

1 – ESP32
‘ESP32 is a highly-integrated and low-power MCU with Wi-Fi and Bluetooth connectivity for IoT applications. It can function as a standalone system or as a slave device to a host MCU, and supports various interfaces and modules’.
Datasheet

2 – DS3231
‘The DS3231 is a low-cost, extremely accurate I²C real-time clock (RTC) with an integrated temperature-compensated crystal oscillator (TCXO) and crystal. The device incorporates a battery input, and maintains accurate timekeeping when main power to the device is interrupted’.
Datasheet

3 – MCP23017
‘The MCP23017/MCP23S17 (MCP23X17) device family provides 16-bit, general purpose parallel I/O expansion for I2C bus or SPI applications’.
Datasheet

4 – JQC-3FF-S-Z Relay
‘JQC-3FF-S-Z is a 10A, PCB type, Form C relay with silver alloy contacts’.
Datasheet

5 – 12 and 5VDC Power Supply
Power supplies stacked on top of each other, with 127VAC input and 5 and 12VDC outputs.

6 – PZEM-004T
‘AC communication module, the module is mainly used for measuring AC voltage, current, active power, frequency, power factor and active energy’.
Datasheet

7 – 24VDC Power Supply
24VDC power supply. It was necessary to remove it from its protective casing so that it would fit in the project. Used to power the humidifier.

8 – Terminal blocks
Terminal blocks used to separate the control of the 127V circuits, such as the fans, lighting, and dehumidifier.

9 – HD-38 Soil Moisture
‘Soil Moisture Sensor Module HD-38 module is designed for precise moisture detection within the soil, enabling accurate monitoring of water content’.
Datasheet

10 – I2C Conection
To improve the connection with the temperature and humidity sensor installed inside the controlled environment, an RJ-11 connector was fixed in place.

11 – BME280
‘BME280 Humidity, Temperature and Pressure Sensor designed for low current consumption (3.6μA at 1Hz), long-term stability, I2C communication, and high EMC robustness’.
Datasheet

Electronic Diagram

During the physical development of the project, it was necessary to create an electronic diagram as a support base for assembling the device panel. This was also done to facilitate maintenance in case of component failure or communication issues.

Next Steps

This is a project that is always under construction. As new features, configurations, or expansions occur, the information contained on this site will be updated. Below, I list some of them that are on the project’s roadmap.

1 – Soil moisture sensor readings

All components are installed and physically working. However, the values read from the measurements show electromagnetic interference when the lighting is on. I am planning to change the project’s architecture to eliminate this issue.

2 – Plant phase definition interface

Currently, the plant phase definition is being set by manually sending an MQTT message to the ‘worg/plant_phase’ topic, which is then written to a .csv file. I plan to develop some graphical way to change these parameters, probably through restricted access on this same website. I am evaluating whether this option is the best in terms of human-machine interaction.

3 – Log monitoring

At certain times that do not follow any specific pattern, the system freezes. Generally, there is a gap of months between each incident. I plan to implement log monitoring via Grafana Loki to better understand the occurrence.

4 – MQTT communication with SSL

Despite several attempts, I had numerous issues with the SSL certificate. For this implementation, the system required a lot of processing and ended up affecting environmental control. I still intend to implement it, perhaps by focusing on thread control or migrating to a programming language that natively provides improved memory and process management.

My CV

Below you can follow my professional journey, stay in touch with me, and get to know me better 🙂