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();
}

