Outsourcing Herb Growing to My Smart Home
For years, I dreamed about growing our own herbs and veggies. But as a renter, every attempt turned into nothing more than pots filled with dirt and broken dreams.
Now that my wife Britt and I have bought our first home, complete with a gorgeous raised garden bed around an outdoor pergola, those dreams are back with a vengeance. We’re aiming to transform that garden into an automated haven of fresh herbs and chillis—with a bit of tech magic to make it as low-maintenance as possible.
The Dream
We want to stop buying expensive herb sleeves from the supermarket. Just using two sleeves a week racks up nearly $350 annually—and half the time they end up wilted in the bin. So our goal is a garden that provides fresh herbs and garnishes year-round.
But before we could plant anything, we needed to clear the old decorative plants and prep the bed. As we did, we realized buying all the seedlings from scratch was going to cost over $500. That’s when we remembered: seeds exist.
The Challenge: Growing Chillis from Seed
While herbs grow fairly easily from seed, chillis are another story. They need consistently warm soil—around 30°C—to germinate. In Victoria, Australia, that kind of consistency is hard to come by, even in summer. So we needed a way to keep the soil warm no matter the weather.
Enter the Smart Greenhouse.
Smart Greenhouse Goals
Our miniature greenhouse would serve as a nursery for our chilli seedlings. But I wasn’t content with just passive warmth—I wanted to log data, automate controls, and nerd out.
Here’s what we needed:
Keep soil temp near 30°C
Maintain at least 30% humidity
Automate heating, cooling, and misting
The Build
Hardware
Greenhouse: A standard mini greenhouse from Bunnings
Fan: 240V exhaust fan to vent heat and humidity
Heating: Waterproof 20W seedling heat mats
Humidity: Irrigation misters controlled via a solenoid valve
Controller: ESP32 board with 4 relays and mains power
Sensors:
DS18B20 temperature probes (for soil)
DHT22 humidity & temp sensor (for air)
Weatherproofing
All electronics went inside a sealed enclosure with cable glands. I mounted the relay board on a removable tray, soldered my sensors and wires, and made sure everything was sealed up tight.
Wiring
One relay for the fan
One for the solenoid valve (running off a 12V transformer)
Two for the heat mats
DS18B20 probes all wired to a single data pin with pull-up resistor
DHT22 wired separately and encased in epoxy for durability
Software & Automations
Everything is running via ESPHome and connected to Home Assistant. I created a dashboard showing live temperatures, humidity, and device status.
Controls
Soil Thermostat: Maintains heat mats at ~30°C
Air Thermostat: Kicks on fan if air temp exceeds 50°C
Humidity Logic: If humidity is >20% off target for 30 mins, the fan or misters activate to bring it back
Daily Watering: Misters run briefly each day to prevent dry soil
NOTE: I also added a function to run the fan periodically, even if no thresholds are hit, to ensure some fresh airflow inside the greenhouse throughout the day.
Everything can be configured via Home Assistant, including desired temps, humidity ranges, and mist durations.
Testing and Early Results
With just basil and coriander planted for testing, the soil temps held steady. Once we filled the greenhouse with pots and trays, the added thermal mass improved temp stability further.
After a few weeks of healthy growth, a storm hit. With insufficient weight in the base, the greenhouse was blown over, spilling the contents of all the pots. I salvaged what I could, repotted the survivors, and carried on. It seems most of what I saved has survived, but it definitely took the wind out of my otherwise very excited sails.
What’s Next
Despite setbacks, things are growing. As of now, our chillis are thriving in their automated, climate-controlled nursery.
Next up:
A Home Assistant-powered irrigation system with fertiliser dosing
A 3D-printed garden lighting setup
And if you’re not a fan of dirt and leaves, don’t worry—there are plenty more smart home and automation projects on the way.
Thanks for reading!
If you'd like to replicate this project, the YAML configuration and STL files are linked below.
Parts
Temp Sensors - Not the same source I used
Humidity / Air Temp Sensor - Not the same source I used
Xconnect Pigtails (waterproof connectors)
YAML
esphome: name: "greenhouse" friendly_name: Greenhouse min_version: 2024.11.0 name_add_mac_suffix: false esp32: board: esp32dev framework: type: esp-idf substitutions: update_interval: 60s log_level: DEBUG relay1_restore_mode: RESTORE_DEFAULT_OFF relay2_restore_mode: RESTORE_DEFAULT_OFF relay3_restore_mode: RESTORE_DEFAULT_OFF relay4_restore_mode: RESTORE_DEFAULT_OFF logger: api: ota: - platform: esphome wifi: ssid: Your_SSID password: Your_Password time: - platform: sntp id: sntp_time servers: - 0.pool.ntp.org - 1.pool.ntp.org - 2.pool.ntp.org update_interval: 6h timezone: "" one_wire: - platform: gpio pin: number: GPIO33 id: x1 sensor: - platform: wifi_signal name: "WiFi Signal dB" id: wifi_signal_db update_interval: ${update_interval} entity_category: "diagnostic" - platform: copy source_id: wifi_signal_db name: "WiFi Signal Percent" filters: - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); unit_of_measurement: "Signal %" entity_category: "diagnostic" device_class: "" - platform: dallas_temp one_wire_id: x1 address: 0xdc00000036a22728 name: Probe_2 id: probe_2 update_interval: 30s - platform: dallas_temp one_wire_id: x1 address: 0x7d00000033b6e328 name: Probe_1 id: probe_1 update_interval: 30s - platform: template name: "Probe_Average" unit_of_measurement: "°C" update_interval: 30s lambda: |- if (isnan(id(probe_1).state) || isnan(id(probe_2).state)) { return NAN; } return (id(probe_1).state + id(probe_2).state) / 2.0; - platform: dht pin: GPIO4 temperature: name: "Air Temp" id: air_temp humidity: name: "Humidity" id: humidity update_interval: 30s switch: - platform: gpio name: "Heater_1" internal: true pin: GPIO27 id: relay1 restore_mode: ${relay1_restore_mode} - platform: gpio name: "Heater_2" internal: true pin: GPIO14 id: relay2 restore_mode: ${relay2_restore_mode} - platform: gpio name: "Fan" pin: GPIO12 id: relay3 restore_mode: ${relay3_restore_mode} - platform: gpio name: "Misters" pin: GPIO13 id: relay4 restore_mode: ${relay4_restore_mode} light: - platform: status_led name: "Status LED" disabled_by_default: true pin: GPIO15 climate: - platform: thermostat id: heater_1 name: "Heater_1" sensor: probe_1 min_idle_time: 30s min_heating_off_time: 60s min_heating_run_time: 60s heat_deadband: 3 heat_action: - switch.turn_on: relay1 idle_action: - switch.turn_off: relay1 visual: min_temperature: 15 max_temperature: 50 temperature_step: 1 - platform: thermostat id: heater_2 name: "Heater_2" sensor: probe_2 min_idle_time: 30s min_heating_off_time: 60s min_heating_run_time: 60s heat_deadband: 3 heat_action: - switch.turn_on: relay2 idle_action: - switch.turn_off: relay2 visual: min_temperature: 15 max_temperature: 50 temperature_step: 1 - platform: thermostat id: air_temp_thermostat name: "Air_Temp" sensor: air_temp min_idle_time: 30s min_cooling_off_time: 60s min_cooling_run_time: 60s cool_deadband: 2 cool_action: - switch.turn_on: relay3 idle_action: - switch.turn_off: relay3 preset: - name: Default default_target_temperature_high: 45 visual: min_temperature: 20 max_temperature: 60 temperature_step: 1 binary_sensor: - platform: homeassistant name: "Disable Greenhouse Automation" entity_id: input_boolean.disable_greenhouse_automation id: disable_automation - platform: template name: "Fan Active" id: fan_active lambda: |- return id(relay3).state; - platform: template name: "Mister Active" id: mister_active lambda: |- return id(relay4).state; - platform: template name: "Heater_1 Active" id: heater_1_active lambda: |- return id(relay1).state; - platform: template name: "Heater_2 Active" id: heater_2_active lambda: |- return id(relay2).state; number: - platform: template name: "Humidity Setpoint" id: humidity_setpoint optimistic: true restore_value: true min_value: 10 max_value: 90 step: 1 unit_of_measurement: "%" - platform: template name: "Mister Duration" id: mister_duration optimistic: true restore_value: true min_value: 2 max_value: 30 step: 1 unit_of_measurement: "s" globals: - id: last_mister_time type: uint32_t restore_value: no initial_value: '0' script: - id: humidity_fan_hold_script mode: restart then: - lambda: |- const uint32_t cooldown_ms = 10 * 60 * 1000; // 10 minutes uint32_t time_since_misters = millis() - id(last_mister_time); if (time_since_misters < cooldown_ms) { ESP_LOGI("fan_hold", "Waiting for mister cooldown to finish (%d seconds remaining)", (cooldown_ms - time_since_misters) / 1000); return; } - if: condition: lambda: |- bool should_run = ( id(humidity).state > id(humidity_setpoint).state + 20 && abs(id(probe_1).state - id(heater_1).target_temperature) <= 10 && abs(id(probe_2).state - id(heater_2).target_temperature) <= 10 && !id(disable_automation).state ); if (!should_run) { ESP_LOGI("fan_hold", "Fan hold skipped: humidity=%.1f, setpoint=%.1f, probe_1=%.1f, target_1=%.1f, probe_2=%.1f, target_2=%.1f, disabled=%d", id(humidity).state, id(humidity_setpoint).state, id(probe_1).state, id(heater_1).target_temperature, id(probe_2).state, id(heater_2).target_temperature, id(disable_automation).state); } return should_run; then: - logger.log: "Humidity high, running fan for minimum 2 minutes" - switch.turn_on: relay3 - delay: 2min - while: condition: lambda: |- return ( id(humidity).state > id(humidity_setpoint).state + 10 && !id(disable_automation).state ); then: - logger.log: "Humidity still elevated, keeping fan ON" - delay: 30s - lambda: |- if (id(air_temp_thermostat).action != CLIMATE_ACTION_COOLING) { ESP_LOGI("fan_hold", "Humidity normalized and thermostat idle, turning fan OFF"); id(relay3).turn_off(); } else { ESP_LOGI("fan_hold", "Humidity normalized, but thermostat is cooling — leaving fan ON"); } interval: - interval: 30s then: - lambda: |- if ( !id(disable_automation).state && (millis() - id(last_mister_time) > 7200000) && id(humidity).state < id(humidity_setpoint).state - 10 ) { ESP_LOGI("mister", "Humidity low, triggering misters"); id(last_mister_time) = millis(); id(relay4).turn_on(); delay(id(mister_duration).state * 1000); id(relay4).turn_off(); }