Skip to content

Instantly share code, notes, and snippets.

@jdrews
Last active September 17, 2023 04:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdrews/727eaa6a57117bb0f2a22e64a80b7537 to your computer and use it in GitHub Desktop.
Save jdrews/727eaa6a57117bb0f2a22e64a80b7537 to your computer and use it in GitHub Desktop.
Collect temp, humidity and dirt moisture (wetness) from monk makes plant monitor (pmon) and a Vegetronix VH400 and ship to influxdb
[Unit]
Description=Plant Monitor Service
After=multi-user.target
[Service]
WorkingDirectory=/home/alarm/git/pmon/raspberry_pi
User=alarm
ExecStart=/usr/bin/python /home/alarm/git/pmon/raspberry_pi/plantmondb.py -i <INFLUXDB_IP>
Type=simple
[Install]
WantedBy=multi-user.target
import time
from plant_monitor import PlantMonitor
from influxdb_client import InfluxDBClient, Point
from datetime import datetime
import argparse
import logging
from logging.handlers import RotatingFileHandler
import serial
# username = 'username'
# password = 'password'
def is_float(element: any) -> bool:
# If you expect None to be passed:
if element is None:
return False
try:
float(element)
return True
except ValueError:
return False
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--influxhost", help="IP of the influxdb host")
parser.add_argument("-p", "--influxport",
help="port of the influxdb host", default="8086")
parser.add_argument("-s", "--polltimer",
help="amount of time in seconds to poll for metrics from sensor", default=60)
parser.add_argument("-d", "--influxdatabase",
help="name of influx database", default="plantmon")
parser.add_argument("-t", "--influxtag",
help="a tag to put for metrics in influx db", default="aspidistra")
parser.add_argument(
"--vh400serial", help="serial port for the vegetronix vh400", default="/dev/ttyUSB0")
args = parser.parse_args()
retention_policy = 'autogen'
bucket = f'{args.influxdatabase}/{retention_policy}'
pm = PlantMonitor()
logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler('plantmon.log', maxBytes=5000, backupCount=5)
logger.addHandler(handler)
ser = serial.Serial(port=args.vh400serial, timeout=3)
try:
while True:
wetness_field = pm.get_wetness()
temp_c_field = pm.get_temp()
humidity_field = pm.get_humidity()
vh400_vwc_field = ser.readline().decode().strip()
# When the Vegetronix VH400 first starts up, it does not output a number ( just /x00). Skip writing this point if so.
if (is_float(vh400_vwc_field)):
logger.info(str(datetime.now()) + " -- Wetness (%): " + str(wetness_field) + ", Temp (C): " + str(
temp_c_field) + ", Humidity (%): " + str(humidity_field) + ", VWC (%): " + str(vh400_vwc_field))
point = Point("plantmon").tag("tag", args.influxtag).field("wetness_percent", wetness_field).field(
"temp_c", temp_c_field).field("humidity_percent", humidity_field).field("vh400_vwc", float(vh400_vwc_field))
else:
logger.info(str(datetime.now()) + " -- Wetness (%): " + str(wetness_field) + ", Temp (C): " + str(
temp_c_field) + ", Humidity (%): " + str(humidity_field))
point = Point("plantmon").tag("tag", args.influxtag).field("wetness_percent", wetness_field).field(
"temp_c", temp_c_field).field("humidity_percent", humidity_field)
with InfluxDBClient(url='http://' + args.influxhost + ':' + args.influxport, org='-') as client:
with client.write_api() as write_api:
logger.info(point.to_line_protocol())
write_api.write(bucket=bucket, record=point)
time.sleep(int(args.polltimer))
except Exception as e:
logger.error('Error at %s', 'plant monitor loop', exc_info=e)
// Set analogPin to the input pin
int inputAnalogPin = 0;
void setup() {
// put your setup code here, to run once:
pinMode(inputAnalogPin, INPUT);
Serial.begin(9600);
}
void loop() {
// put your main code here, to run repeatedly:
Serial.println(readVH400(inputAnalogPin));
delay(1000);
}
// MIT License (MIT)
//
// Copyright (c) 2015. Michael Ewald, GeomaticsResearch LLC.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Author: Michael Ewald (mewald@geomaticsresearch.com)
// Web: https://GeomaticsResearch.com
// Last-updated: 2015-07-04
// From: https://gist.github.com/lx-88/413b48ced6b79300ea76
float readVH400(int analogPin) {
// This function returns Volumetric Water Content by converting the analogPin value to voltage
// and then converting voltage to VWC using the piecewise regressions provided by the manufacturer
// at http://www.vegetronix.com/Products/VH400/VH400-Piecewise-Curve.phtml
// UPDATE 2023-09-16: above page moved to http://www.vegetronix.com/Products/VH400/VH400-Piecewise-Curve
// NOTE: You need to set analogPin to input in your setup block
// ex. pinMode(<analogPin>, INPUT);
// replace <analogPin> with the number of the pin you're going to read from.
// Read value and convert to voltage
int sensor1DN = analogRead(analogPin);
float sensorVoltage = sensor1DN*(5.0 / 1023.0);
float VWC;
// Calculate VWC
if(sensorVoltage <= 1.1) {
VWC = 10*sensorVoltage-1;
} else if(sensorVoltage > 1.1 && sensorVoltage <= 1.3) {
VWC = 25*sensorVoltage-17.5;
} else if(sensorVoltage > 1.3 && sensorVoltage <= 1.82) {
VWC = 48.08*sensorVoltage-47.5;
} else if(sensorVoltage > 1.82) {
VWC = 26.32*sensorVoltage-7.89;
}
return(VWC);
}
struct VH400 {
double analogValue;
double analogValue_sd;
double voltage;
double voltage_sd;
double VWC;
double VWC_sd;
};
struct VH400 readVH400_wStats(int analogPin, int nMeasurements = 100, int delayBetweenMeasurements = 50) {
// This variant calculates the mean and standard deviation of 100 measurements over 5 seconds.
// It reports mean and standard deviation for the analog value, voltage, and WVC.
// This function returns Volumetric Water Content by converting the analogPin value to voltage
// and then converting voltage to VWC using the piecewise regressions provided by the manufacturer
// at http://www.vegetronix.com/Products/VH400/VH400-Piecewise-Curve.phtml
// NOTE: You need to set analogPin to input in your setup block
// ex. pinMode(<analogPin>, INPUT);
// replace <analogPin> with the number of the pin you're going to read from.
struct VH400 result;
// Sums for calculating statistics
int sensorDNsum = 0;
double sensorVoltageSum = 0.0;
double sensorVWCSum = 0.0;
double sqDevSum_DN = 0.0;
double sqDevSum_volts = 0.0;
double sqDevSum_VWC = 0.0;
// Arrays to hold multiple measurements
int sensorDNs[nMeasurements];
double sensorVoltages[nMeasurements];
double sensorVWCs[nMeasurements];
// Make measurements and add to arrays
for (int i = 0; i < nMeasurements; i++) {
// Read value and convert to voltage
int sensorDN = analogRead(analogPin);
double sensorVoltage = sensorDN*(5.0 / 1023.0);
// Calculate VWC
float VWC;
if(sensorVoltage <= 1.1) {
VWC = 10*sensorVoltage-1;
} else if(sensorVoltage > 1.1 && sensorVoltage <= 1.3) {
VWC = 25*sensorVoltage-17.5;
} else if(sensorVoltage > 1.3 && sensorVoltage <= 1.82) {
VWC = 48.08*sensorVoltage-47.5;
} else if(sensorVoltage > 1.82) {
VWC = 26.32*sensorVoltage-7.89;
}
// Add to statistics sums
sensorDNsum += sensorDN;
sensorVoltageSum += sensorVoltage;
sensorVWCSum += VWC;
// Add to arrays
sensorDNs[i] = sensorDN;
sensorVoltages[i] = sensorVoltage;
sensorVWCs[i] = VWC;
// Wait for next measurement
delay(delayBetweenMeasurements);
}
// Calculate means
double DN_mean = double(sensorDNsum)/double(nMeasurements);
double volts_mean = sensorVoltageSum/double(nMeasurements);
double VWC_mean = sensorVWCSum/double(nMeasurements);
// Loop back through to calculate SD
for (int i = 0; i < nMeasurements; i++) {
sqDevSum_DN += pow((DN_mean - double(sensorDNs[i])), 2);
sqDevSum_volts += pow((volts_mean - double(sensorVoltages[i])), 2);
sqDevSum_VWC += pow((VWC_mean - double(sensorVWCs[i])), 2);
}
double DN_stDev = sqrt(sqDevSum_DN/double(nMeasurements));
double volts_stDev = sqrt(sqDevSum_volts/double(nMeasurements));
double VWC_stDev = sqrt(sqDevSum_VWC/double(nMeasurements));
// Setup the output struct
result.analogValue = DN_mean;
result.analogValue_sd = DN_stDev;
result.voltage = volts_mean;
result.voltage_sd = volts_stDev;
result.VWC = VWC_mean;
result.VWC_sd = VWC_stDev;
// Return the result
return(result);
}
@jdrews
Copy link
Author

jdrews commented Aug 7, 2023

Tested on archlinuxarm on a Raspberry Pi 3 using Python 3.11.3

The sensor is a Monk Makes Plant Monitor from https://monkmakes.com/pmon
Source for sensor is here: https://github.com/monkmakes/pmon

@jdrews
Copy link
Author

jdrews commented Sep 17, 2023

Now I added another sensor, a Vegetronix VH400 from https://vegetronix.com/soil-moisture-sensor

The Vegetronix VH400 connected to an Arduino Diecimila over USB and outputs the Volumetric Water Content (VWC) over serial on /dev/ttyUSB0 on the Raspberry Pi.
If you don't have that sensor, just comment out the reading and change the influxdb point writing.

@jdrews
Copy link
Author

jdrews commented Sep 17, 2023

Added the source code running on the Arduino Diecimila that reads from the Vegetronix VH400 over analog pin 0.
This code was sourced from: https://gist.github.com/lx-88/413b48ced6b79300ea76

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment