Do you love plants and have so many that you find it hard to keep track of each plant's needs? Let's take your plants to the next level so they can tell you when they're not feeling well!
With this smart plant pot powered by MicroPython and integrated with Home Assistant, plant care reaches a new level. By utilizing sensors to monitor factors such as soil moisture, light levels, and temperature, this technology allows you to proactively care for your plants and visualize their health using LED lights if your standing right in front of it or Home Assistant if you want to look after your little friend from far away :)
To kickstart the hardware setup, we're going to need all the components linked above, including the intriguing 3D printed parts and, of course, the meticulous wiring of the components.
3D Printed Parts
Let's begin our journey with the fascinating world of 3D printed parts! The plant pot has been split into two pieces. The lower part is where we can put the PSOC6 board, the power supply board, and the temperature sensor. On the left side of the bottom part, there's a special section to keep the electronic parts safe if any water gets inside. When you swipe to the next picture, you'll be able to see how all the parts fit together.
Now in the top part we include the light, moisture and CO2 sensor. In the pictures below you can see where they are placed.
The LED panel has a small section in the front. In the next picture you can see a picture of the top part upside down. You should have the wiring of the two light sensors and the LED panel coming out.
Finally, if you put both parts together and your plant in, it will all look like this:
Schematics
For this project, we've opted for MicroPython as our programming language of choice, because it's easy to use and compatible with our PSOC6 board. If you're new to MicroPython, we've prepared a handy protip to guide you through the installation process.
Once the MicroPython is running on your board, we can start with our code.
Our code is divided into three parts to facilitate its understanding:
Part 1: Code for the Plant1. Installing Required Modules
The required sensor drivers and modules for Home Assistant integration can be downloaded using these commands:
The installation of the required modules for sensor interaction and Home Assistant integration will be handled below in part 3, the main script. For now, we assume these packages to be installed already.
2. Importing Required Modules
import machine
import time
import sht30
import pasco2
import stemma_soil_sensor
import seesaw
import face
Our program begins by importing necessary modules, including hardware interaction libraries like machine, timing functions from time, and specific sensor libraries such as sht30, pasco2, and stemma_soil_sensor. A separate face module is also included to handle visual feedback (code will be explained down bellow).
3. Pin Definitions
PIN_SCL = 'P6_0'
PIN_SDA = 'P6_1'
PIN_LUX1 = 'P10_0'
PIN_LUX2 = 'P10_3'
- PIN_SCL and PIN_SDA: I2C communication pins used for connecting sensors.
- PIN_LUX1 and PIN_LUX2: Pins connected to light sensors.
4. Fuzzy Logic Operator Function
def fuzzy_operator(value, minValue, maxValue, relevance=2):
'''
Returns a value larger than 0 if the value is outside the range.
The higher the value, the further away from the optimal range.
'''
if not value:
return 0
if value > maxValue:
x = value - maxValue
elif value < minValue:
x = minValue - value
else:
return 0
return relevance * x / (maxValue - minValue)
The fuzzy_operator() function measures how much a value (e.g., temperature, humidity) deviates from its ideal range. If the value is too high/too low, it calculates how much it exceeds/falls below the maxValue/minValue. If the value is within range (minValue to maxValue), it returns 0 (no issue).
The result is weighted by the relevance factor (default is 2).
5. Plant Class Definition
class Plant:
condition = None
temperature = None
humidity = None
moisture = None
light = None
co2 = None
update_timer = 0
Here, we define a Plant class to model the condition of our plant through tracking various environmental parameters:
- temperature, humidity, moisture, light, co2: These store sensor readings.
- condition: A score representing the plant’s health (from 0 to 3).
- update_timer: Helps regulate how often CO2 measurements are updated.
6. Initializing the Plant Class
def __init__(self, minTemp=16, maxTemp=23, minHum=45, maxHum=60, minMoisture=600, maxMoisture=1400, minLight=30000, maxLight=33000, minCO2=600, maxCO2=1600):
self.minTemp = minTemp
self.maxTemp = maxTemp
self.minHum = minHum
self.maxHum = maxHum
self.minMoisture = minMoisture
self.maxMoisture = maxMoisture
self.minLight = minLight
self.maxLight = maxLight
self.minCO2 = minCO2
self.maxCO2 = maxCO2
The__init__() method sets up optimal conditions to define a healthy plant, which change depending on your plant type:
- Temperature (default: 16°C to 23°C)
- Humidity (45% to 60%)
- Soil Moisture (600 to 1400)
- Light Levels (30, 000 to 33, 000)
- CO2 Concentration (600 to 1600 ppm)
7. String Representation
def __str__(self):
return f"Temperature: {self.temperature}, Humidity: {self.humidity}, Moisture: {se lf.moisture}, Light: {self.light}, CO2: {self.co2}, Condition: {self.condition}"
This methode __str__() defines how a Plant object is displayed when printed, making debugging and logging easier.
8. Checking Plant Condition
def check_condition(self):
'''
Check if the plant is in good condition.
Will set the condition to a score between 3 (best) and 0 (worst).
'''
condition = 3
condition -= fuzzy_operator(self.temperature, self.minTemp, self.maxTemp)
condition -= fuzzy_operator(self.humidity, self.minHum, self.maxHum)
condition -= fuzzy_operator(self.moisture, self.minMoisture, self.maxMoisture)
condition -= fuzzy_operator(self.light, self.minLight, self.maxLight)
condition -= fuzzy_operator(self.co2, self.minCO2, self.maxCO2)
self.condition = int(max(0, round(condition, 0)))
The plant’s condition starts at 3 (best) and decreases based on how far each sensor reading strays from the ideal range.
The fuzzy_operator() function calculates this deviation, assigning a penalty for values that are too high or low. After checking all parameters, the final score is rounded and kept between 0 (plant is dead) and 3 (plant in a very good state). This approach is based on fuzzy logic, which helps handle gradual changes instead of strict pass/fail rules. For more on fuzzy logic, click here.
9. Sensor Initialization
def init_sensors(self):
# I2C interface for PAS CO2, SHT30, DPS368
i2c = machine.I2C(scl='P6_0', sda='P6_1', freq=400000)
# I2C sensors
self.sensor_sht = sht30.SHT30(i2c, i2c_address=sht30.ALTERNATE_I2C_ADDRESS)
self.sensor_co2 = pasco2.PASCO2(i2c, measInterval=60)
self.sensor_co2.initialize()
self.sensor_soil = stemma_soil_sensor.StemmaSoilSensor(i2c)
self.sensor_lux1 = machine.ADC(PIN_LUX1)
self.sensor_lux2 = machine.ADC(PIN_LUX2)
- I2C communication bus.
- Initialization for : Temperature & humidity sensor (SHT30)CO2 sensor (PASCO2)Soil moisture sensor (StemmaSoilSensor)Light sensors (connected to ADC pins)
10. Reading Sensor Data
def read_sensors(self):
self.temperature, self.humidity = self.sensor_sht.measure()
self.moisture = self.sensor_soil.get_moisture()
self.light = (self.sensor_lux1.read_u16() + self.sensor_lux2.read_u16()) / 2
if time.time() - self.update_timer > 60:
co2ppm = self.sensor_co2.get_co2_value()
if co2ppm > 0:
self.co2 = co2ppm # Take CO2 value only if ready.
self.update_timer = time.time()
This method retrieves real-time data from the temperature, humidity, soil moisture, and light sensors. CO2 levels are updated only once per minute to optimize performance.
11. Updating the Face Display
def update_face(self):
if self.condition == 3:
face.show_face(face.get_happy_pattern())
elif self.condition == 2:
face.show_face(face.get_neutral_pattern())
elif self.condition == 1:
face.show_face(face.get_sad_pattern())
else:
face.show_face(face.get_dead_pattern())
The update_face() method uses the computed condition score to select an appropriate facial expression, ranging from happy to dead, which is then displayed to indicate plant health.(code for this in Part 3 down bellow)
12. Main Loop
def loop(self):
self.read_sensors()
self.check_condition()
self.update_face()
Finally, the loop() method ties everything together by continuously reading sensor data, evaluating the plant’s condition, and updating the display. This system provides an intuitive way to monitor plant well-being, making it particularly useful for automated plant care applications.
Part 2 : Code for Face State1. Importing Required Modules and setting up the LED Matrix
from machine import Pin, bitstream
from time import sleep
# Define the timing for the bitstream based on the LED's datasheet
timing = [500, 1125, 800, 750]
PIN_LEDS = Pin('P9_6', Pin.OUT, value=0)
num_leds = 256 # 16x16 matrix has 256 LEDs
led_data = bytearray(num_leds * 3) # Each LED requires 3 bytes for RGB
def create_color(red, green, blue, brightness=0.2):
red = int(red * brightness)
green = int(green * brightness)
blue = int(blue * brightness)
return bytearray([green, red, blue])
def clear_leds():
global led_data
for i in range(num_leds * 3):
led_data[i] = 0
def set_led(x, y, color):
if 0 <= x < 16 and 0 <= y < 16:
if x % 2 == 0:
index = (x * 16) + y # Normal order for even columns
else:
index = (x * 16) + (15 - y) # Reverse order for odd columns
led_data[index * 3: (index + 1) * 3] = color
You might recognize this coding approach from our New Year´s Hat project. The main difference here is that we are working with a 16x16 matrix, which required some adjustments to fit our current project.
2. Defining the "Dead" Face (contion 0)
def get_dead_pattern():
'''
Define the eyes, mouth, and details of the dead face
'''
eyes = [(4, 4), (3, 5), (5, 5), (5, 3),
(11, 4), (12, 3), (12, 5),
(3, 3), (10, 3), (10, 5)] # Coordinates for the eyes
mouth = [(6, 10), (7, 10), (8, 10), (11, 11), (10, 10), (5, 10),
(4, 11), (9, 10)]
# Define color
color = create_color(255, 0, 0) # Red
return eyes, mouth, color
This function defines the "dead" face by returning three values:
eyes
: A list of(x, y)
coordinates for eye placement.mouth
: A set of coordinates forming a straight or distorted mouth.color
: A red color (255, 0, 0
), symbolizing distress. (picture bellow)
2. Defining the "Sad" Face (condition 1 )
def get_sad_pattern():
eyes = [(4, 4), (3, 5), (5, 4), (4, 3), (3, 4), (4, 5), (5, 5), (5, 3),
(11, 5), (11, 4), (11, 3), (12, 4), (12, 3), (12, 5), (10, 4),
(3, 3), (10, 3), (10, 5)]
mouth = [(6, 10), (7, 10), (8, 10), (11, 11), (10, 10), (5, 10),
(4, 11), (7, 9), (8, 9), (9, 9), (6, 9), (9, 10)]
color = create_color(255, 0, 0) # Red
return eyes, mouth, color
This sad face has slightly curved eyes and a frown, also in the color Red.
3. Defining the "Neutral" Face (condition 2 )
def get_neutral_pattern():
eyes = [(4, 4), (3, 5), (5, 4), (4, 3), (3, 4), (4, 5), (5, 5), (5, 3),
(11, 5), (11, 4), (11, 3), (12, 4), (12, 3), (12, 5), (10, 4),
(3, 3), (10, 3), (10, 5)]
mouth = [(6, 10), (7, 10), (8, 10), (11, 10), (10, 10), (5, 10),
(4, 10), (7, 9), (8, 9), (9, 9), (6, 9), (9, 10)]
color = create_color(255, 255, 0) # Yellow
return eyes, mouth, color
The neutral face uses a straight mouth and the yellow color.
4. Defining the "Happy" Face (condition 3 )
def get_happy_pattern():
eyes = [(4, 4), (3, 5), (5, 4), (4, 3), (3, 4), (4, 5), (5, 5), (5, 3),
(11, 5), (11, 4), (11, 3), (12, 4), (12, 5), (10, 4), (10, 3), (10, 5)]
mouth = [(6, 11), (7, 11), (8, 11), (9, 11), (10, 10), (5, 10),
(11, 9), (4, 9), (7, 10), (8, 10), (9, 10), (6, 10)]
color = create_color(255, 255, 255)
return eyes, mouth, color
Codes a smiley face with heart eyes in pink.
5. Displaying a Face
def show_face(pattern):
clear_leds()
eyes, mouth, color = pattern
for (x, y) in eyes:
set_led(x, y, color)
for (x, y) in mouth:
set_led(x, y, color)
bitstream(PIN_LEDS, 0, timing, led_data)
This function clears the LED matrix, draws the face, and updates the display allowing our plant to express its emotions!
Part 3: Main Script1.Set-Up
Before you can run this setup make sure the attached files wifi_secrets.py and mqtt_secrets.py are modified according to your needs and uploaded to the MicroPython device.
import time
import network
import wifi_secrets, mqtt_secrets
sta = network.WLAN(network.STA_IF)
sta.connect(wifi_secrets.ssid, wifi_secrets.psk)
# mip module ("mip installs packages")
import mip
# Adafruit SHT30
mip.install('github:ederjc/micropython-sht30/sht30.py')
# Infineon PAS CO2
mip.install("https://raw.githubusercontent.com/jaenrig-ifx/micropython-lib/mpy-lib/pasco2-sensor-module/micropython/drivers/sensor/pasco2/pasco2.py")
# Adafruit Seesaw Soil Sensor
mip.install('github:mihai-dinculescu/micropython-adafruit-drivers/seesaw/stemma_soil_sensor.py')
mip.install('github:mihai-dinculescu/micropython-adafruit-drivers/seesaw/seesaw.py')
# umqtt
mip.install('umqtt.simple')
import umqtt.simple
# uhome
mip.install('github:ederjc/uhome/uhome/uhome.py')
import uhome
from plant import Plant
### Plant Setup ###
plant = Plant()
plant.init_sensors()
### Home Assistant Integration ###
device = uhome.Device('Smart Plant', mf='Infineon', mdl='PSOC 6')
mqttc = umqtt.simple.MQTTClient(device.id, mqtt_secrets.broker, user=mqtt_secrets.user, password=mqtt_secrets.password, keepalive=60)
device.connect(mqttc)
temperature = uhome.Sensor(device, 'Temperature', device_class="temperature", unit_of_measurement='°C')
humidity = uhome.Sensor(device, 'Air Humidity', device_class="humidity", unit_of_measurement='%')
co2 = uhome.Sensor(device, 'CO2', device_class="carbon_dioxide", unit_of_measurement='ppm')
brightness = uhome.Sensor(device, 'Brightness', device_class="illuminance", unit_of_measurement='lx')
moisture = uhome.Sensor(device, 'Soil Moisture', device_class="moisture", unit_of_measurement='%')
condition = uhome.Sensor(device, 'Condition', device_class="enum", icon="mdi:flower")
This code snipet is responsible for connecting it to Wi-Fi, installing necessary sensor drivers, initializing the plant monitoring sensors, and preparing integration with Home Assistant via MQTT. We have a detailled Protip on this called Anything to Home Assistant. Check it out!
2. Sensor Update
def update_ha_sensors():
temperature.publish(plant.temperature)
humidity.publish(plant.humidity)
co2.publish(plant.co2)
brightness.publish(plant.light)
moisture.publish(plant.moisture/20)
condition.publish(plant.condition)
This function updates sensor readings by publishing the latest values to Home Assistant. The moisture sensor value is divided by 20 to normalize it within a percentage range (since its raw values might be high).
3. Enabling Device Discovery in Home Assistant
device.discover_all()
This makes the "Smart Plant" device visible in Home Assistant, allowing us to find and configure it easily. Isn´t that GREAT!!
In this screeshot you can see all what we need about our Smart plant!
4. Main Loop
while 1:
plant.loop()
print(plant)
update_ha_sensors()
time.sleep(5)
The program enters an infinite loop, continuously monitoring the plant:
- plant.loop(): reads sensor data.
- print(plant): displays sensor values in the console (for debugging).
- update_ha_sensors(): sends updated data to Home Assistant via MQTT.
- time.sleep(5) introduces a 5-second delay before the next update, reducing unnecessary network traffic.
The Smart Plant Pot has plenty of room for growth. Future enhancements could include automated watering, improved sensor accuracy, and expanded connectivity to monitor multiple plants at once. By refining data analysis and integrating more environmental factors, we can make plant care even more intuitive and reliable.
🌿🔧Innovate, automate, cultivate!🌍🌿
Comments
Please log in or sign up to comment.