Making Your Plant Talk with Home Assistant, Alexa and Soil Moisture Sensors
Companion guide for the video
In the video above, I built a soil moisture monitoring system that gives my plant—Patricia—a personality. Instead of silently logging data, Home Assistant turns soil moisture readings into spoken announcements from Alexa, with randomized messages and personality.
This article documents the exact Home Assistant templates and automations used in the project so you can replicate or extend it.
Overview of the System
The project has four main components:
-
Soil moisture sensor (hardware)
-
Calibrated moisture template sensor (voltage → % moisture)
-
Plant state classifier (
distressed,dry,happy) -
Two Home Assistant automations
- State-change announcements
- Daily check-in announcement
A helper (input_text) stores the plant’s previous state so we can detect
recovery events.
Hardware Notes (VegeHub + VH400)
For this project, I used the VegeHub to ingest analog sensor data into Home Assistant. The VegeHub is a fast, low-friction way to get agricultural sensors online without building custom firmware or wiring up ADCs manually.
For soil moisture, the VH400 capacitive soil moisture sensor is an excellent option. It outputs a stable analog voltage, is not prone to corrosion like resistive probes, and works well indoors.
Step 1: Create the Template Sensors
Add the following to your configuration.yaml (or a templates YAML file,
depending on your setup).
⚠ You MUST update the sensor name
Replace sensor.vege_50_10_input_1 with the analog input sensor on your system.
template:
- sensor:
- name: "Plant 1 Moisture Percent Calibrated"
unit_of_measurement: "%"
state_class: measurement
device_class: humidity
state: >
{% set v = states('sensor.vege_50_10_input_1') | float(0) %}
{% if v <= 0.0 %}
{% set value = 0 %}
{% elif v <= 1.1 %}
{% set value = (v - 0.0) / (1.1 - 0.0) * 10 %}
{% elif v <= 1.3 %}
{% set value = (v - 1.1) / (1.3 - 1.1) * 5 + 10 %}
{% elif v <= 1.82 %}
{% set value = (v - 1.3) / (1.82 - 1.3) * 25 + 15 %}
{% elif v <= 2.2 %}
{% set value = (v - 1.82) / (2.2 - 1.82) * 10 + 40 %}
{% elif v <= 3.0 %}
{% set value = (v - 2.2) / (3.0 - 2.2) * 50 + 50 %}
{% else %}
{% set value = 100 %}
{% endif %}
{{ value | round(1) }}
- name: "Plant 1 State"
state: >
{% set c = states('sensor.plant_1_moisture_percent_calibrated') | float(0) %}
{% if c < 15 %}
distressed
{% elif c < 25 %}
dry
{% else %}
happy
{% endif %}
What this does
- Converts raw voltage to calibrated soil moisture percentage
- Classifies the plant into three emotional states
- Makes automation logic far easier and readable
Step 2: Create the Helper
In Home Assistant:
Settings → Devices & Services → Helpers → Create Helper → Text
Name it:
Plant Last State
Entity ID should be:
input_text.plant_last_state
This stores the previous plant state so we can detect recovery events.
Step 3: Main Announcement Automation
This automation announces only when the plant state changes, with randomized messages.
alias: Patricia Plant Announcements
triggers:
- entity_id: sensor.plant_1_state
trigger: state
actions:
- choose:
- conditions:
- condition: template
value_template: >-
{{ current_state not in ['unknown', 'unavailable'] and
current_state != last_state }}
- condition: time
after: "08:00:00"
before: "21:00:00"
sequence:
- data:
target: media_player.the_office
message: >
{% if current_state == 'happy' and last_state in ['distressed',
'dry'] %}
{{ recovered_messages | random }}
{% elif current_state == 'distressed' %}
{{ distressed_messages | random }}
{% elif current_state == 'dry' %}
{{ dry_messages | random }}
{% elif current_state == 'happy' %}
{{ happy_messages | random }}
{% endif %}
data:
type: announce
action: notify.alexa_media
- data:
entity_id: input_text.plant_last_state
value: "{{ current_state }}"
action: input_text.set_value
mode: single
variables:
current_state: >-
{{ trigger.to_state.state if trigger.to_state is defined else
states('sensor.plant_1_state') }}
last_state: "{{ states('input_text.plant_last_state') }}"
distressed_messages:
- Patricia needs you. She can't water herself.
- I’m concerned about Patricia. She's feeling parched.
- Patricia is in distress. Please water her.
- You have a responsibility. Patricia needs your care.
- Patricia is very uncomfortable right now. Please help her.
dry_messages:
- Patricia is getting uncomfortable.
- Patricia would appreciate a little water.
- The soil is a bit dry. Patricia and I have noticed.
- "Just a reminder: Patricia is a bit thirsty."
- Patricia is getting a bit thirsty. She will need water soon.
happy_messages:
- Patricia is doing well. Good job.
- The soil moisture looks good. Patricia and I approve.
- Patricia is comfortable right now. Keep up the good work.
- Patricia is healthy and content. Thank you for taking care of her.
recovered_messages:
- Patricia is feeling better now. Thank you for helping her.
- Thank you for watering Patricia. We both appreciate it.
- Patricia has recovered. I'm glad you saved her.
- The soil moisture is back to a healthy level. Patricia is happy again.
- Patricia will remember this. You did a great job taking care of her.
Important Customizations
- Replace
media_player.the_officewith your Echo / Alexa target - Ensure Alexa Media Player is installed and working
- Change the plant name or messages however you want
Step 4: Daily Check-In Automation
If you miss an alert (sleeping, away, muted speakers), this automation ensures you hear the plant’s status at least once per day.
alias: Patricia Plant Check-In
triggers:
- at: "09:00:00"
trigger: time
actions:
- choose:
- conditions:
- condition: template
value_template: "{{ current_state not in ['unknown', 'unavailable'] }}"
sequence:
- data:
target: media_player.the_office
message: >
{% if current_state == 'happy' and last_state in ['distressed',
'dry'] %}
{{ recovered_messages | random }}
{% elif current_state == 'distressed' %}
{{ distressed_messages | random }}
{% elif current_state == 'dry' %}
{{ dry_messages | random }}
{% elif current_state == 'happy' %}
{{ happy_messages | random }}
{% endif %}
data:
type: announce
action: notify.alexa_media
mode: single
variables:
current_state: >-
{{ trigger.to_state.state if trigger.to_state is defined else
states('sensor.plant_1_state') }}
last_state: "{{ states('input_text.plant_last_state') }}"
distressed_messages:
- Patricia needs you. She can't water herself.
- I’m concerned about Patricia. She's feeling parched.
- Patricia is in distress. Please water her.
- You have a responsibility. Patricia needs your care.
- Patricia is very uncomfortable right now. Please help her.
dry_messages:
- Patricia is getting uncomfortable.
- Patricia would appreciate a little water.
- The soil is a bit dry. Patricia and I have noticed.
- "Just a reminder: Patricia is a bit thirsty."
- Patricia is getting a bit thirsty. She will need water soon.
happy_messages:
- Patricia is doing well. Good job.
- The soil moisture looks good. Patricia and I approve.
- Patricia is comfortable right now. Keep up the good work.
- Patricia is healthy and content. Thank you for taking care of her.
recovered_messages:
- Patricia is feeling better now. Thank you for helping her.
- Thank you for watering Patricia. We both appreciate it.
- Patricia has recovered. I'm glad you saved her.
- The soil moisture is back to a healthy level. Patricia is happy again.
- Patricia will remember this. You did a great job taking care of her.
Customization Notes
You will almost certainly need to modify:
- Sensor names (
sensor.vege_50_10_input_1,sensor.plant_1_state) - Alexa media player entity
- Moisture thresholds (
15%,25%) - Voltage calibration curve for your specific soil and sensor
The calibration curve in this example is tailored for a VH400 in potting soil. Your soil, pot size, and plant type will differ.
Why This Approach Works
Most plant monitoring projects stop at dashboards. This system:
- Converts raw voltage → calibrated moisture
- Converts moisture → semantic plant states
- Converts plant states → spoken personality-driven notifications
It’s not just telemetry. It’s human-friendly feedback.
