Node-Red, Pi and interfacing Adafruit hardware

This is a special forum devoted to educators using Adafruit and Arduino products for teaching.

Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.
User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

After a long time, I actually have a Raspberry Pi interfacing with 4* Stemma soil mosture and temperature sensors, an SGP30 for CO2 level measurement and the BME 280 for temperature and humidity in a 4X4 grow tent. I hope to take this past the prototype stage, but to be honest it is a little overwhelming.

Once I have the monitoring right, I want to move to automation (watering, lights etc).

My hardware skills are good enough, my soldering skills need more time, they werre better a long time ago.

I am fluent in many scripting languages, but the development of dashboards and automation is challenging. Until 2 weeks ago, I barely knew what Python was, Javascript I was okay with, but it failed. I had no desire to write a dashboard for this from scratch - a key reason why it took me so long to do.

IThe I2C nodes in Node-Red could not query most of the Adafruit hardware, moving to Javascript and I2C-Bus in Node.js just gave gibberish. That was another reason why it took longer than I wanted.

In this first post, I will share how to build (in very point form) how I built the base devices I have for this purpose. Leter I'll include mote detail and make it easier to understand and follow. Who's with me?
Attachments
Checklist.pdf
(582.69 KiB) Downloaded 16 times

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

So, I think I have it...
Flows.txt
Current dashboard
(19.58 KiB) Downloaded 16 times
{u}Lessons learned:[/u]
Stemma soil sensor
1. Don't try to finesse the sensors in the python code. (I have one bad sensor and I think I used that for development, the newer ones - post the last upgrade seem to be working fine. With the code, simplest is best.)
2.The values will wander, I'm still not sure that I have the flows right, but using Smooth seems to work just fine - for the working sensors.
3. There is a runtime error thrown by the seesaw.py library where the deice hardware id returned is 0xd5 where 0x55 is expected. It happens at random it seems with all the sensors I have. I had to alter the code to not send the runtime error. Too many of those from a node crashes Node-Red.
4. Hand watering causes the sensors to have furry fits. It might be the soil, it might be the fertilizer. I can't wait to implement a gravity fed drip feed.

SGP30
1. ppm CO2 is reported as 400 even if it is under. Working on it.,

BME280
This darned thing rocks....

The dashboard to date:
Flows.txt
Current dashboard
(19.58 KiB) Downloaded 16 times
JSON Flows attached.
Attachments
Current dashboard
Current dashboard
Dashboard.JPG (134.44 KiB) Viewed 4977 times

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

A visual of the nodes...
Flows.JPG
Flows.JPG (172.84 KiB) Viewed 4977 times

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

I am not liking this editor....

I apologize, I had intended the flows to be attached and the dashboard to be embedded. They got reversed.

I'm sure you know, now you know I do as well.

User avatar
Franklin97355
 
Posts: 23911
Joined: Mon Apr 21, 2008 2:33 pm

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by Franklin97355 »

Thanks for the work. I'm sure it will help others doing the same.

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

The real trick is...

1. Make it work in Python as per the Adafruit docs
2. Make sure you have the Python3 function installed
3. Use the example code from the website as a starter.

Returning the values uses a Python dictionary where msg contains a "Topic" and a "Payload"

The rest is just learning Node-Red and Python

If you are doing this all on the same I2C BUS then remember
1. The maximum length of 22 AWG unshielded is 1 meter

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

Fat fingers...

Stemma - poll slowly, every 20 seconds per sensor seems optimal
Capacitance takes time to build up.
Don't trust the temp sensor, it's useful though.

I2C bus - ALWAYS stagger your transactions, give 1 second delay between transactions for the bus to clear and stabilize.

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

Well, it's been a rough couple of days, but productive.

The Stemma Soil sensors, you don't have to be so gentle on them. The problem I had was wiring - mostly. I got to looking at I2C bus extenders and found that they rely mostly on Cat5. I wish I could find the original post where someone provided the math. If anyone has the math, I'd be glad to see it again.

As it is, I could extend the Stemma cables reliably to at least 6 feet which is long enough to reach from the Pi on the side of the tent to all the fabric plant pots in the grow tent. I put VCC and GND on one pair, SDA and SCL on another and cut the other two pairs off. I'm now polling them every 7 seconds with no issues.

However, they still give the occasional issue - a Runtime error that tends to cause Node Red issues, as in crashing. Solution? Modify seesaw.py (/usr/local/lib/python3.7/dist-packages/adafruit_seesaw/seesaw.py) to comment lines 144-150. It's not ideal, but it does work.

The BME280 is working like a champ still. I'm planning on using its data in the next cut to provide absolute humidity for correction to the SGP30 CO2 data.

Yes, I installed a tank, regulator and math based controls (for now) for CO2 enrichment. I control it all from Node Red using an HL-52S IOT relay. Unfortunately, not in Adafruit's inventory and I highly recommend that they carry it. I could have used the IOT relay they carry, but it was out of stock and this looked like a good replacement even it was less finished and took a little more thining to make work.

Since I had a free relay, I also added heat controls and am using a 350 watt heater until the mats all come in. Each mat is 150 watts, so combined, they should work as a space heater replacement.

That being said... The SGP30... I've been beating my head trying to make it work. It never gave a reading over 400PPM and I could not figure out why until I looked at the code for the library. It turns out that the Python3 function for Node Red instantiates the class on each run. Inside that code is an initialization call which meant I would never get a reading of the current CO2 level. So, the answer first is to save and restore the Baselines as needed. Then modify adafruit_sgp30.py (/usr/local/lib/python3.7/dist-packages/ adafruit_sgp30.py) to comment line 58 (might not be a good idea, but it works once a baseline is established and restored as needed)

I'll go into more detail later on the code and stuff, but for now will leave a snapshot of the Version3 dashboard.
Dashboard Version3.JPG
Dashboard Version3.JPG (97.19 KiB) Viewed 4833 times

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

The code for the Stemma Soil sensors in the Python3 function node is pretty easy. I just modified the example from the website to return the data in the mgs1 and msg2 dictionaries appropriately:

Code: Select all

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import time
from board import SCL, SDA
import busio
from adafruit_seesaw.seesaw import Seesaw
i2c_bus = busio.I2C(SCL, SDA)
ss = Seesaw(i2c_bus, addr=0x36)
# read temperature from the sensor
msg1 = {
    "topic": "Temp",
    "payload": round(ss.get_temp(),2)
}
time.sleep(1)
# read moisture from the sensor
msg2 = {
    "topic": "Moisture",
    "payload": round(ss.moisture_read(),2)
}
return [msg1,msg2]
I don't think this needs much explaining... If anyone has questions, feel free to reply.

One note about the Python3 function node - it's unsupported. The other thing to note is - don't expect be be able to read any data passed to it in the msg dictionary. The upshot is, it's easier to write the data to an environment variable (which persists across functions, but gets reset on deployment) or a text file for use in other functions.

I used the first method in the SGP30 function, and the latter in the BME280 function. I needed to calculate absolute humidity to pass to the SGP30 so I added that as well.

Code: Select all

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import time
import board
import busio
import adafruit_bme280
from datetime import datetime
import math 
# Create library object using our Bus I2C port
i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
 
# OR create library object using our Bus SPI port
# spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
# bme_cs = digitalio.DigitalInOut(board.D10)
# bme280 = adafruit_bme280.Adafruit_BME280_SPI(spi, bme_cs)
 
# change this to match the location's pressure (hPa) at sea level
bme280.sea_level_pressure = 1013.25
# Read the sensors
thistemp = bme280.temperature
thisrh = bme280.relative_humidity
thispressure = bme280.pressure
thisalt  = bme280.altitude
# calc the absolute humidity
"""https://carnotcycle.BANNED.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/"""
absTemperature = thistemp + 273.15;
absHumidity = 6.112;
absHumidity *= math.exp((17.67 * thistemp) / (243.5 + thistemp));
absHumidity *= thisrh;
absHumidity *= 2.1674;
absHumidity /= absTemperature;
# save the data to /home/pi/BME280.txt
thisfile = "/home/pi/BME280.txt"
with open(thisfile, 'w') as file:
  file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + "\n")
  file.write(str(thistemp) + "\n")
  file.write(str(thisrh) + "\n")
  file.write(str(thispressure) + "\n")
  file.write(str(thisalt) + "\n")
  file.write(str(absHumidity) + "\n")
# read temperature from the sensor
msg1 = {
    "topic": "Temp",
    "payload": round(thistemp,2)
}
# read moisture from the sensor
msg2 = {
    "topic": "Humid",
    "payload": round(thisrh,2)
}
# read barometric pressure from the sensor
msg3 = {
    "topic": "Pressure",
    "payload": round(thispressure,2)
}
# read altitude from the sensor
msg4 = {
    "topic": "Altitude",
    "payload": round(thisalt,2)
}
return [msg1,msg2,msg3,msg4]
Again, code was shamelessly copied, pasted and modified from many sources. I don't proclaim to be an expert in Python and this is the fastest way.

Finally, the SGP30 (remember to comment out the line 58 in the library):

Code: Select all

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
""" Example for using the SGP30 with CircuitPython and the Adafruit library"""
import os
import time
import board
import busio
import adafruit_sgp30
from datetime import datetime
from os import path
i2c = busio.I2C(board.SCL, board.SDA, frequency=100000)
# Create library object on our I2C port
sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)
# This will be used for debugging
message = ""
# Check the environment variable to see if this is the first frequency
firstrun = False
try:
  # This is actually just a test to see if the environment variable is set
  if (os.environ['SGP30'] == "On"):
    firstrun = False
  message = "Querying"
except:
    # The environment variable will not exist, causing an error
    firstrun = True
# Check for and load last baseline date and values
BaseFile = "/home/pi/SGP30.txt"
if path.exists(BaseFile):
  with open(BaseFile, 'r') as file:
    lst = [line.rstrip() for line in file]
  LastSave = datetime.strptime(lst[0], '%Y-%m-%d %H:%M:%S')
  eCO2Base = int(lst[1],16)
  TVOCBase = int(lst[2],16)
  # Initialize the SGP30 if this is the first run after the deployment
  if firstrun:
    # Initialize the SGP30
    sgp30.iaq_init()
    # Set the Baseline
    sgp30.set_iaq_baseline(eCO2Base, TVOCBase)
    # set the environment variable
    os.environ['SGP30'] = "On"
    message = "Initializing..."
  else:
    # get the existing baseline from the SGP30
    NeweCO2Base = sgp30.baseline_eCO2
    NewTVOCBase = sgp30.baseline_TVOC
    # Only update if the new values are not 0
    if NeweCO2Base !=0 and NewTVOCBase !=0:
      # If they don't match then save the new Baselines
      if eCO2Base != NeweCO2Base and TVOCBase != NewTVOCBase:
        with open(BaseFile, 'w') as file:
          file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + "\n")
          file.write(hex(NeweCO2Base) + "\n")
          file.write(hex(NewTVOCBase) + "\n")
        message = "Changed baseline saved."
else:
  # Use the default Baselines
  eCO2Base = 0x8973
  TVOCBase = 0x8AAE
  # Initialize the SGP30
  sgp30.iaq_init()
  # Set the Baseline
  sgp30.set_iaq_baseline(eCO2Base, TVOCBase)
  with open(BaseFile, 'w') as file:
    file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + "\n")
    file.write(hex(eCO2Base)+ "\n")
    file.write(hex(TVOCBase)+ "\n")
  message = "Baseline file created."
# Check for the BME280.txt file so we can load the absolute humidity
BMEFile = "/home/pi/BME280.txt"
if path.exists(BMEFile):
  with open(BMEFile, 'r') as file:
    lst = [line.rstrip() for line in file]
  absHum = float(lst[5])
  # Now send that the the sgp30
  sgp30.set_iaq_humidity(absHum)
# read CO2 ppm from the sensor
eCO2 = sgp30.eCO2
TVOC = sgp30.TVOC
# Set up the return values
msg1 = {
  "topic": "CO2",
  "payload": eCO2
}
# read TVOC from the sensor
msg2 = {
  "topic": "TVOC",
  "payload": TVOC
}
# read TVOC from the sensor
msg3 = {
  "topic": "Status",
  "payload": message
}
return [msg1,msg2,msg3]
In this case, I use an environment variable to detect if this is the first run after the deployment. Note that it saves the Baselines in /home/pi/SGP30.txt for re-use, and if the baselines change, the file is updated. It also loads the absolute humidity from /home/pi/BME280.txt (if it exists) and saves it to the SGP30.

Next, I'll get into how to get these into a Node-Red flow and dashboard. There's enough learning resources out there, so it will be high level.

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

Oh yes, the flows. For those fluent in Node Red, a pic and check the attachment for the JSON flow data (for import)
Flows Version3.JPG
Flows Version3.JPG (181.73 KiB) Viewed 4828 times
Attachments
Flows Version3.txt
(30.78 KiB) Downloaded 9 times

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

Well, it is finally done. The SGP30 turned out to be the hardest part, both to understand and implement. It also was the most crucial part to get right if I wanted to perform CO2 enrichment.

The Stemmas are all working perfectly, the BME280 provided some challenges and the SGP30 was just a real trick to understand. Thanks to the folks at Sensiron, they made some things quite clear.

Node Red might have been the easiest dashboarding platform to use but it really presented challenges. I went with using the Python3 function node, but there were things I didn't understand about how anything worked. I think I finally solved it.

So, the Stemmas were mostly a cabling issue, I was using 4 wire AWG22 to connect them at the start. Cat5 allows me to run them at least 6 feet without a problem.

The BME280 and SGP30 both had issues that one would find specific to Node-Red. The python code is pretty straightforward but running it in a Python3 function node was not. The common issue with both was that one each run the class had to be instantiated. In each of the Init routines there were resets for the device to initialize it. While necessary, it really only needs to be done when the device is first powered up. Some code modifications to the adafruit libraries helped that.

The SGP30 was a mystery, especially with humidity compensation and the calibration. I saw lots of code for calculating the absolute humidity, but most of it produced it in mg/m3 and in the youtube videos I saw, nobody was implementing it even though they had the code. I can guarantee you that the SGP30 python library needs it in g/m3.

The SGP should be initialized after the first power up. If you save a baseline after the iaq_init, the SGP30 will continue on happily without calibrating until it feels the need to update the baselines. Essentially, the SGP30 is constantly calibrating itself and updating the baselines so watching each baseline change and waiting for it to stop is really a waste of time.

If you just took it out of the bag, then you can use the "inceptive" TVOC baseline that is stored on the device to make things a little more accurate and faster. It will ONLY work for SGP30's with a featureset of 34 (decimal) or above and ONLY on the first time, don't use it afterwards.

The problem is, the python library and example don't support querying it or setting only the TVOC baseline as per Sensiron's documentation. A few code modifications later and I was up and running with a new SGP30.

Additionally, I figured that it would be more effecient to read both CO2 and TVOC at the same time, as well with their baselines. It takes 4 reads down to 2, speeding up the functions.

The following changes can be made to /usr/local/lib/python3.7/dist-packages/ adafruit_sgp30.py to add the featureset query, inceptive TVOC baseline query and set the TVOC baseline:

Change the code in line 55 to read:

Code: Select all

featureset = self.get_iaq_featureset
Add the following to the @Properties section:

Code: Select all

    @property
    # pylint: disable=invalid-name
    def featureset(self):
        """SGP30 Featureset"""
        return self.get_iaq_featureset()
        
@property
    # pylint: disable=invalid-name
    def inceptive_baseline(self):
        """SGP30 Inceptive TVOC Baseline"""
        return self.get_iaq_inceptive_baseline()[0]

    @property
    # pylint: disable=invalid-name
    def measure(self):
        """Get both TVOC and CO2"""
        return self.iaq_measure()         

    @property
    # pylint: disable=invalid-name
    def baselines(self):
        """TVOC and Carbon Dioxide Equivalent baseline values"""
        return self.get_iaq_baseline()
This code has to be added at the end of the class code:

Code: Select all

    def get_iaq_featureset(self):
        """Get the SGP30 featureset"""
        # name, command, signals, delay
        return self._run_profile(["iaq_featureset", [0x20, 0x2F], 1, 0.01])

    def get_iaq_inceptive_baseline(self):
        """Retreive the IAQ inceptive baseline for TVOC"""
        # name, command, signals, delay
        return self._run_profile(["iaq_get_inceptive_baseline", [0x20, 0xb3], 1, 0.01])

    def set_iaq_TVOC_baseline(self, TVOC):  # pylint: disable=invalid-name
        """Set the baseline for TVOC"""
        if TVOC == 0:
            raise RuntimeError("Invalid baseline")
        buffer = []
        for value in [TVOC]:
            arr = [value >> 8, value & 0xFF]
            arr.append(self._generate_crc(arr))
            buffer += arr
        self._run_profile(["iaq_set_TVOC_baseline", [0x20, 0x77] + buffer, 0, 0.01])
To make the library work in Node-Red, you also need to comment out line 58:

Code: Select all

        # self.iaq_init()
Note that this means you need to detect the startup and then perform the iaq_init in your code at the right time.

If you run an iaq_init and don't set a baseline after it, you end up in an accelerated calibration. Personally, I found it faster than using the inceptive baseline. However, it added one twist to the project. The SGP30 is optimized for a query every second. The calibration completes in 12 hours, but I am 100% sure it is tied to this query rate, with the dashboard, I was limited to a query every 5 seconds (since solved and reduced to 1 second) and if you do the math, a calibration could take 60 hours. I'm not that patient.

Now, the BME.

It worked fine in Node-Red until I had it tied into the SGP code and running once every second. Sometimes it would not complete the reset quickly enough and that caused issues. So I modified the code in /usr/local/lib/python3.7/dist-packages/ adafruit_bme280.py as follows:

This replaces the old _init_

Code: Select all

    # pylint: disable=too-many-instance-attributes
    def __init__(self):
        """Check the BME280 was found, read the coefficients and enable the sensor"""
        # Check device ID.
        chip_id = self._read_byte(_BME280_REGISTER_CHIPID)
        if _BME280_CHIPID != chip_id:
            raise RuntimeError("Failed to find BME280! Chip ID 0x%x" % chip_id)
        # Set some reasonable defaults.
        self._iir_filter = IIR_FILTER_DISABLE
        self._overscan_humidity = OVERSCAN_X1
        self._overscan_temperature = OVERSCAN_X1
        self._overscan_pressure = OVERSCAN_X16
        self._t_standby = STANDBY_TC_125
        self._mode = MODE_SLEEP
        self._read_coefficients()
        self.sea_level_pressure = 1013.25
        """Pressure in hectoPascals at sea level. Used to calibrate `altitude`."""
        self._t_fine = None
And this needs to be added to the class to ensure you can manually initialize the BME280:

Code: Select all

    def firststart(self):
        self._reset()
        self._read_coefficients()
        self._write_ctrl_meas()
        self._write_config()
        self.sea_level_pressure = 1013.25
        """Pressure in hectoPascals at sea level. Used to calibrate `altitude`."""
        self._t_fine = None
So now, how to query the Stemmas every 5 seconds and the SGP/BME combination every 1 second without causing issues on the I2C bus? Add another I2C bus. I followed the instructions here: https://www.instructables.com/Raspberry ... c-Devices/

Now the function code had to be altered to work on I2C Bus 3. The was accomplished thanks to this library: https://circuitpython.readthedocs.io/pr ... en/latest/

And finally, to tie it all together, I have a script "SGP30_BME280_test.py"

Code: Select all

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
""" Example for using the SGP30 with CircuitPython and the Adafruit library"""
import time
import board
import busio
import math 
import adafruit_sgp30
import adafruit_bme280
from adafruit_extended_bus import ExtendedI2C as I2C

from datetime import datetime
# Create library object on our I2C port
# i2c = busio.I2C(board.SCL, board.SDA, frequency=100000)
i2c = I2C(3) # Device is /dev/i2c-1
sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
# Get then featureset
print("Featureset is: " + str(sgp30.featureset))
# Get the inceptive Baseline
Inceptive = sgp30.inceptive_baseline
print("Inceptive TVOC baseline is: " + hex(Inceptive))
# Get the serial number and turn it into a string
SGPSerial = "-".join([hex(i) for i in sgp30.serial]).replace("0x","")
# Check for and load last baseline date and values
BaseFile = "/home/pi/SGP30_" + SGPSerial + ".txt"
try:
  with open(BaseFile, 'r') as file:
    lst = [line.rstrip() for line in file]
  LastSave = datetime.strptime(lst[0], '%Y-%m-%d %H:%M:%S')
  eCO2Base = int(lst[1],16)
  TVOCBase = int(lst[2],16)
  print("Baseline file is: " + BaseFile)
  # So if the baselines were loaded from the file
  if input("Restore old Baselines (Y/N): ").upper() == "Y":
    sgp30.iaq_init
    sgp30.set_iaq_baseline(eCO2Base, TVOCBase)
    print("Old Baseline restored.")
  else:
    sgp30.iaq_init
    print("No Baseline set.")
except:
  eCO2Base = 0xFFFF
  TVOCBase = 0xFFFF
  print("No Baseline file.")
  if input("Is this the first calibration (Y/N): ").upper() == "Y":
    sgp30.iaq_init
    sgp30.set_iaq_TVOC_baseline(Inceptive)
    print("Inceptive Baseline applied.")
  else:
    sgp30.iaq_init
    print("No Baseline set.")
# Begin processing
elapsed_sec = 0
FirstTen = True
# change this to match the location's pressure (hPa) at sea level (optimized for Calgary)
bme280.sea_level_pressure = 1018.50
while True:
  # Read the sensors
  thistemp = bme280.temperature
  thisrh = bme280.relative_humidity
  thispressure = bme280.pressure
  thisalt  = bme280.altitude
  # calc the absolute humidity in g/mg3
  """https://carnotcycle.BANNED.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/"""
  absTemperature = thistemp + 273.15;
  absHumidity = 6.112;
  absHumidity *= math.exp((17.67 * thistemp) / (243.5 + thistemp));
  absHumidity *= thisrh;
  absHumidity *= 2.1674;
  absHumidity /= absTemperature;
  # According to the chip manual and all sources, this must be g/m3
  sgp30.set_iaq_humidity(absHumidity)
  # Test to see if we can read both at the same time
  readings = sgp30.measure
  eCO2 = readings[0]
  TVOC = readings[1]
  # Output what we have
  print("eCO2 = %d ppm \t TVOC = %d ppb" % (eCO2, TVOC))
  # print("eCO2 = %d ppm \t TVOC = %d ppb" % (sgp30.eCO2, sgp30.TVOC))
  # Sleep a minimum of 1 second
  time.sleep(1)
  # Increment our counters
  elapsed_sec += 1
  # Every tenth iteration, output the Baselines
  if elapsed_sec > 20:
    # Output the BME280 stuff for scenery
    print("** BME\tTemp = %2.2f C\tRel Hum = %2.2f %%\tAbs Hum = %2.2f g/m3" % (thistemp, thisrh,absHumidity))
    print("** BME\tAlt = %d\tBar Press = %d" % (thisalt, thispressure))
    elapsed_sec = 0
    FirstTen = False
    # get the existing baseline from the SGP30
    readings = sgp30.baselines
    NeweCO2Base = readings[0]
    NewTVOCBase = readings[1]
    # If they don't match then save the new Baselines
    if eCO2Base != NeweCO2Base or TVOCBase != NewTVOCBase:
      with open(BaseFile, 'w') as file:
        file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + "\n")
        file.write(hex(NeweCO2Base) + "\n")
        file.write(hex(NewTVOCBase) + "\n")
        eCO2Base = NeweCO2Base
        TVOCBase = NewTVOCBase
        print("**** New Baseline saved.")
      # Output the Gas data
      print("** SGP\teCO2 = 0x%x\tTVOC = 0x%x" % (NeweCO2Base, NewTVOCBase))
It's clearly a "work in progress" some of the comments need to be updated and there are a few legacy bits that do nothing and need to be deleted, but it works well enough.

I think that's enough learning for this week. Happy to respond to any questions. Next week I'll get into the IOT relay wiring and programming.

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

Just realized there was a piece missing. Here's a better copy of the code:

Code: Select all

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
""" Example for using the SGP30 with CircuitPython and the Adafruit library"""
import time
import board
import busio
import math 
from adafruit_extended_bus import ExtendedI2C as I2C
import adafruit_sgp30
import adafruit_bme280

from datetime import datetime
# Create library object on our I2C port
# i2c = busio.I2C(board.SCL, board.SDA, frequency=100000)
i2c = I2C(1) # Device is /dev/i2c-1
sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
# Initialize the BME280
bme280.firststart
# Get then featureset
print("Featureset is: " + str(sgp30.featureset))
# Get the inceptive Baseline
Inceptive = sgp30.inceptive_baseline
print("Inceptive TVOC baseline is: " + hex(Inceptive))
# Get the serial number and turn it into a string
SGPSerial = "-".join([hex(i) for i in sgp30.serial]).replace("0x","")
# Check for and load last baseline date and values
BaseFile = "/home/pi/SGP30_" + SGPSerial + ".txt"
try:
  with open(BaseFile, 'r') as file:
    lst = [line.rstrip() for line in file]
  LastSave = datetime.strptime(lst[0], '%Y-%m-%d %H:%M:%S')
  eCO2Base = int(lst[1],16)
  TVOCBase = int(lst[2],16)
  print("Baseline file is: " + BaseFile)
  # So if the baselines were loaded from the file
  if input("Restore old Baselines (Y/N): ").upper() == "Y":
    sgp30.iaq_init
    sgp30.set_iaq_baseline(eCO2Base, TVOCBase)
    print("Old Baseline restored.")
  else:
    sgp30.iaq_init
    print("No Baseline set.")
except:
  eCO2Base = 0xFFFF
  TVOCBase = 0xFFFF
  print("No Baseline file.")
  if input("Is this the first calibration (Y/N): ").upper() == "Y":
    sgp30.iaq_init
    sgp30.set_iaq_TVOC_baseline(Inceptive)
    print("Inceptive Baseline applied.")
  else:
    sgp30.iaq_init
    print("No Baseline set.")
# Begin processing
elapsed_sec = 0
while True:
  # Read the sensors
  thistemp = bme280.temperature
  thisrh = bme280.relative_humidity
  thispressure = bme280.pressure
  thisalt  = bme280.altitude
  # calc the absolute humidity in g/mg3
  """https://carnotcycle.BANNED.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/"""
  absTemperature = thistemp + 273.15;
  absHumidity = 6.112;
  absHumidity *= math.exp((17.67 * thistemp) / (243.5 + thistemp));
  absHumidity *= thisrh;
  absHumidity *= 2.1674;
  absHumidity /= absTemperature;
  # According to the chip manual and all sources, this must be g/m3
  sgp30.set_iaq_humidity(absHumidity)
  # Test to see if we can read both at the same time
  readings = sgp30.measure
  eCO2 = readings[0]
  TVOC = readings[1]
  # Output what we have
  print("eCO2 = %d ppm \t TVOC = %d ppb" % (eCO2, TVOC))
  # print("eCO2 = %d ppm \t TVOC = %d ppb" % (sgp30.eCO2, sgp30.TVOC))
  # Sleep a minimum of 1 second
  time.sleep(1)
  # Increment our counters
  elapsed_sec += 1
  # Every twentieth iteration, output the Baselines
  if elapsed_sec > 20:
    # Output the BME280 stuff for scenery
    print("** BME\tTemp = %2.2f C\tRel Hum = %2.2f %%\tAbs Hum = %2.2f g/m3" % (thistemp, thisrh,absHumidity))
    print("** BME\tAlt = %d\tBar Press = %d" % (thisalt, thispressure))
    elapsed_sec = 0
    # get the existing baseline from the SGP30
    readings = sgp30.baselines
    NeweCO2Base = readings[0]
    NewTVOCBase = readings[1]
    # If they don't match then save the new Baselines
    if eCO2Base != NeweCO2Base or TVOCBase != NewTVOCBase:
      with open(BaseFile, 'w') as file:
        file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + "\n")
        file.write(hex(NeweCO2Base) + "\n")
        file.write(hex(NewTVOCBase) + "\n")
        eCO2Base = NeweCO2Base
        TVOCBase = NewTVOCBase
        print("**** New Baseline saved.")
      # Output the Baseline data
      print("** SGP\teCO2 = 0x%x\tTVOC = 0x%x" % (NeweCO2Base, NewTVOCBase))

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

Node Red Python3 Function node issue is solved.

So, the one big issue I had was properly handling inputs to a Python3 function node.

The fix (which I have tested) is available from here: https://github.com/dejanrizvan/node-red ... unction.js

Copy the contents into /home/pi/.node-red/node_modules/node-red-contrib-python3-function/lib/python3-function.js

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

An update on my current state:

1. Stemma soil sensors
I want to break these out to a seperate "project" which is to monitor and water the plants as needed. I have noticed that when the soil temperature exceeds the external temperature, then it is a sign the plants need watering. The sensors themselves are not providing the expected values, but I suspect that this is a problem related to the soil composition, in that the plants I am monitoring now are in a heavy peat+compost/potting soil mix and it does not drain well, but it does hold moisture. I think that is what is throwing my sensors off and in the next crop, I will confirm this. The code works though, and Node-Red (after the code changes) seems to be working well.
Capture1.JPG
Capture1.JPG (14.53 KiB) Viewed 4665 times
The flow looks like this:
Capture2.JPG
Capture2.JPG (25.98 KiB) Viewed 4665 times
I use a 5 second "Inject" node (because there are 3 other Stemmas on the same bus). If you have one plant being monitored, you could increase the frequency, but when you then go and graph it over time, it can "overwhelm" Node-Red and the web page starts becoming unresponsive and having issues.

You can use this JSON to create your own flow:

Code: Select all

[{"id":"5e673c8.c4e2ac4","type":"inject","z":"394fcece.136712","name":"5 sec","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":150,"y":500,"wires":[["75948a30.36a7e4"]]},{"id":"75948a30.36a7e4","type":"python3-function","z":"394fcece.136712","name":"Left Rear Soil","func":"# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries\n# SPDX-License-Identifier: MIT\nimport time\nfrom board import SCL, SDA\nimport busio\nfrom adafruit_seesaw.seesaw import Seesaw\ni2c_bus = busio.I2C(SCL, SDA)\nss = Seesaw(i2c_bus, addr=0x36)\ntime.sleep(.25)\n# read temperature from the sensor\nmsg1 = {\n    \"topic\": \"Temp\",\n    \"payload\": round(ss.get_temp(),2)\n}\n# read moisture from the sensor\nmsg2 = {\n    \"topic\": \"Moisture\",\n    \"payload\": round(ss.moisture_read(),2)\n}\nreturn [msg1,msg2]","outputs":2,"x":260,"y":560,"wires":[["e8e94eae.794e6"],["a05772bb.fee55"]]},{"id":"e8e94eae.794e6","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"6","round":"2","mult":"single","reduce":false,"x":460,"y":540,"wires":[["df24129e.1f131"]]},{"id":"a05772bb.fee55","type":"smooth","z":"394fcece.136712","name":"","property":"payload","action":"mean","count":"6","round":"0","mult":"single","reduce":false,"x":460,"y":580,"wires":[["24b1a656.02afaa"]]},{"id":"df24129e.1f131","type":"ui_gauge","z":"394fcece.136712","name":"","group":"dba29111.3e4d1","order":2,"width":3,"height":2,"gtype":"gage","title":"Temp","label":"C","format":"{{value}}","min":"15","max":"35","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"22","seg2":"30","x":590,"y":540,"wires":[]},{"id":"24b1a656.02afaa","type":"ui_gauge","z":"394fcece.136712","name":"","group":"dba29111.3e4d1","order":1,"width":3,"height":2,"gtype":"gage","title":"Moisture","label":"","format":"{{value}}","min":"500","max":"1500","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"800","seg2":"1200","x":600,"y":580,"wires":[]},{"id":"dba29111.3e4d1","type":"ui_group","name":"(x36) Soil Left Rear","tab":"fc7ad6e.40b8028","order":1,"disp":true,"width":6,"collapse":true},{"id":"fc7ad6e.40b8028","type":"ui_tab","name":"Grow Tent","icon":"dashboard","order":2,"disabled":false,"hidden":false}]
When you connect more than one device on the same I2C bus, you only have one choice for the low. You need to "cascade" the devices so the first triggers the second, and so on, like this:
Capture3.JPG
Capture3.JPG (56.61 KiB) Viewed 4665 times
I took the readings and use the smooth node to average them just so the guages didn't jump so drastically.

User avatar
iwbaxter
 
Posts: 22
Joined: Fri Feb 19, 2021 12:49 am

Re: Node-Red, Pi and interfacing Adafruit hardware

Post by iwbaxter »

2. Temperature and Humidity
I initially used the BME280 and SGP30 for this, but I am reconsidering because I want the CO2 sensor to be the primary source for control of CO2 injection into the tent. Having had one bad encounter with a faulty flowmeter (leaks like a sieve) and my own carelessness (didn't check it for leaks), I am a little paranoid about what I would use for that purpose. The SCD40 breakout may be a lot more accurate, and it also has a temp/humidity sensor built in. I am thinking of replacing this combination (although good for an environmental sensor) with the SCD40 for higher accuracy.

However, the BME280 is a good sensor, and with the code changes I outlined previously, it works like a champ. The flow I have looks like this:
Capture4.JPG
Capture4.JPG (37.24 KiB) Viewed 4815 times
The high temperature and humidity are dealt with by a fan system, not controlled by the Pi yet. The fan is PWM controlled, but the vendor has declined to work with me to modify it, so I have another one chosen as a replacement, just not yet. That would be another project.

I also noticed that the better solution for controlling humidity would be a dehumidifier, which I am waiting to arrive. It's also dumping CO2 from the tent into my living room, so a better solution must be found.

However, low temperature, I do have handled with a small 350 watt heater. It's a simple, manual device with no thermostat and no other controls than an on/off switch. This makes it ideal for the IOT relays I use to control them, I simply turn the outlet on or off when heat is needed through this flow:
Capture5.JPG
Capture5.JPG (53.83 KiB) Viewed 4815 times
This is the flow (remember, it is tied to the SGP30):

Code: Select all

[{"id":"f12b409.03550c","type":"python3-function","z":"394fcece.136712","name":"SGP30","func":"# Adafruit can have the copyright, this is for free use\nimport os\nfrom os import path\nfrom datetime import datetime, timedelta\nimport time\nimport math\n#\n#import the necessary Adafruit libraries\nfrom adafruit_extended_bus import ExtendedI2C as I2C\nimport adafruit_sgp30\nimport adafruit_bme280\n#\n# Device is /dev/i2c-3\ni2c = I2C(3)\n#\n# Create library object using our Bus I2C port\nsgp30 = adafruit_sgp30.Adafruit_SGP30(i2c)\nbme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)\n#\n# See if this is the first run\ntry:\n  # Get the last time the SGP was restarted\n  SGPStart = datetime.strptime(os.environ[\"SGPStart\"], '%Y-%m-%d %H:%M:%S')\n  # If we got here without error, then this is not the first iteration after a start\n  FirstRun = False\nexcept:\n  # Load the SGPStart variable\n  SGPStart = datetime.now()\n  # Set the environment for the start of the deployment\n  os.environ[\"SGPStart\"] = datetime.strftime(SGPStart, '%Y-%m-%d %H:%M:%S')\n  # This is the first run of the deployment\n  FirstRun = True\n  # Set up the bme280 (perform reset, etc)\n  bme280.firststart\n#\n# Get the SGP serial number and turn it into a string\nSGPSerial = \"-\".join([hex(i) for i in sgp30.serial]).replace(\"0x\",\"\")\n# Create the Serial specific Baseline file name\nBaseFile = \"/home/pi/SGP30_\" + SGPSerial + \".txt\"\n# Check for and load last baseline date and values\nif path.exists(BaseFile):\n  with open(BaseFile, 'r') as file:\n    lst = [line.rstrip() for line in file]\n  # Last time we started a calibration sequence\n  LastCalibration = datetime.strptime(lst[0], '%Y-%m-%d %H:%M:%S')\n  # Last time was saved the baselines\n  LastSave = datetime.strptime(lst[1], '%Y-%m-%d %H:%M:%S')\n  # Load the existing baseline values\n  eCO2Base = int(lst[2],16)\n  TVOCBase = int(lst[3],16)\nelse:\n  # Set these to invalid dates\n  LastCalibration = datetime.strptime(\"1970-01-01 00:00:00\", '%Y-%m-%d %H:%M:%S')\n  LastSave = datetime.strptime(\"1970-01-01 00:00:00\", '%Y-%m-%d %H:%M:%S')\n  # Set invalid baseline values\n  eCO2Base = 0\n  TVOCBase = 0\n#\n# If this is the first run, then initialize the SGP30\nif FirstRun:\n  # Initialize the SGP30\n  sgp30.iaq_init()\n  # Check for an old baseline\n  BaselineOld = SGPStart - LastSave\n  # Divide by 24 Hours * 60 minutes * 60 seconds to get days\n  BaselineOld = BaselineOld.total_seconds() / 24*60*60\n  # 7 or more days old is not valid\n  if BaselineOld < 7:\n    # Set the Baseline\n    sgp30.set_iaq_baseline(eCO2Base, TVOCBase)\n    # Do not reset the Last Calibration Date\n    # Set the message\n    message = \"Starting...\"\n  else:\n    # The SGP is going to calibrate\n    LastCalibration = datetime.now()\n    # Save the state\n    with open(BaseFile, 'w') as file:\n      file.write(datetime.strftime(LastCalibration, '%Y-%m-%d %H:%M:%S') + \"\\n\")\n      file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + \"\\n\")\n      file.write(hex(eCO2Base) + \"\\n\")\n      file.write(hex(TVOCBase) + \"\\n\")\n    # Set the message\n    message = \"Calibrating...\"\nelse:\n  # Get how many hours since the last calibration\n  CalibOld = datetime.now() - LastCalibration\n  # Divide by 60 minutes * 60 seconds to get hours\n  CalibOld = CalibOld.total_seconds() / 3600\n  # Only check and save the Baseline if it has been 12 hours since the last calibration\n  if CalibOld > 12:\n    # Get how many minutes since the last baseline save\n    BaselineOld = datetime.now() - LastSave\n    # Divide by 60 seconds to get minutes\n    BaselineOld = BaselineOld.total_seconds() / 60\n    # Only retrieve and save if it's been an hour\n    if BaselineOld > 59:\n      # get the existing baseline from the SGP30\n      readings = sgp30.baselines\n      eCO2Base = readings[0]\n      TVOCBase = readings[1]\n      with open(BaseFile, 'w') as file:\n        file.write(datetime.strftime(LastCalibration, '%Y-%m-%d %H:%M:%S') + \"\\n\")\n        file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + \"\\n\")\n        file.write(hex(eCO2Base) + \"\\n\")\n        file.write(hex(TVOCBase) + \"\\n\")\n      # This will be used for debugging\n      message = \"Baseline saved...\"\n    else:\n      # This will be used for debugging\n      message = \"Querying...\"\n  else:\n    # This will be used for debugging\n    message = \"Calibrating...\"      \n# Read the sensors\ntime.sleep(.5)\nthistemp = bme280.temperature\nthisrh = bme280.relative_humidity\n# calc the absolute humidity\n\"\"\"https://carnotcycle.BANNED.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/\"\"\"\nabsTemperature = thistemp + 273.15;\nabsHumidity = 6.112;\nabsHumidity *= math.exp((17.67 * thistemp) / (243.5 + thistemp));\nabsHumidity *= thisrh;\nabsHumidity *= 2.1674;\nabsHumidity /= absTemperature;\nsgp30.set_iaq_humidity(absHumidity)\n# read CO2 ppm from the sensor\nreadings = sgp30.measure\neCO2 = readings[0]\nTVOC = readings[1]\n# write the data we need for CO2 control\nCO2File = \"/home/pi/SGP30.txt\"\nwith open(CO2File, 'w') as file:\n  file.write(datetime.strftime(SGPStart, '%Y-%m-%d %H:%M:%S') + \"\\n\")\n  file.write(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S') + \"\\n\")\n  file.write(str(eCO2) + \"\\n\")\n  file.write(str(TVOC) + \"\\n\")\n# Set up the return values\n# CO2 PPM\nmsg1 = {\n  \"topic\": \"CO2\",\n  \"payload\": eCO2\n}\n# TVOC PPB\nmsg2 = {\n  \"topic\": \"TVOC\",\n  \"payload\": TVOC\n}\n# Debug message\nmsg3 = {\n  \"topic\": \"Status\",\n  \"payload\": message\n}\n# read temperature from the sensor\nmsg4 = {\n    \"topic\": \"Temp\",\n    \"payload\": round(thistemp,2)\n}\n# read moisture from the sensor\nmsg5 = {\n    \"topic\": \"Humid\",\n    \"payload\": round(thisrh,2)\n}\n# Send it\nreturn [msg1,msg2,msg3,msg4,msg5]","outputs":5,"x":220,"y":120,"wires":[[],[],[],["d6f0e66d.b954d8","a52fa8f4.2584e8"],[]]},{"id":"fd8eb990.89e448","type":"ui_chart","z":"394fcece.136712","name":"","group":"54991333.36cc3c","order":4,"width":9,"height":3,"label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"bezier","nodata":"","dot":false,"ymin":"20","ymax":"30","removeOlder":"2","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f76b4","#ff7f0e","#9467bd","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":790,"y":100,"wires":[[]]},{"id":"336a5d64.79aa02","type":"ui_gauge","z":"394fcece.136712","name":"","group":"54991333.36cc3c","order":3,"width":3,"height":3,"gtype":"gage","title":"Temp","label":"C","format":"{{value}}","min":"20","max":"30","colors":["#ffff6e","#00b500","#ffff6e"],"seg1":"25","seg2":"27","x":790,"y":140,"wires":[]},{"id":"7a68b6ba.b07c68","type":"smooth","z":"394fcece.136712","name":"Calc Min","property":"payload","action":"min","count":"360","round":"2","mult":"single","reduce":false,"x":520,"y":160,"wires":[["fa5ce8a2.520e98"]]},{"id":"fa5ce8a2.520e98","type":"change","z":"394fcece.136712","name":"Min","rules":[{"t":"change","p":"topic","pt":"msg","from":"Temp","fromt":"str","to":"Min","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":650,"y":160,"wires":[["fd8eb990.89e448"]]},{"id":"ee67fbe.e613308","type":"smooth","z":"394fcece.136712","name":"Calc Max","property":"payload","action":"max","count":"360","round":"2","mult":"single","reduce":false,"x":520,"y":80,"wires":[["7b15ae1f.411f5"]]},{"id":"7b15ae1f.411f5","type":"change","z":"394fcece.136712","name":"Max","rules":[{"t":"change","p":"topic","pt":"msg","from":"Temp","fromt":"str","to":"Max","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":650,"y":80,"wires":[["fd8eb990.89e448"]]},{"id":"a52fa8f4.2584e8","type":"switch","z":"394fcece.136712","name":"Heat switch","property":"payload","propertyType":"msg","rules":[{"t":"gte","v":"26.40","vt":"str"},{"t":"lte","v":"25","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":350,"y":240,"wires":[["852beccf.7902d"],["a4b8a596.b58208"]]},{"id":"a4b8a596.b58208","type":"python3-function","z":"394fcece.136712","name":"Heat On","func":"import RPi.GPIO as GPIO\nfrom time import sleep\nRelay_channel = 17\nGPIO.setwarnings(False)\nGPIO.setmode(GPIO.BCM)\nGPIO.setup(Relay_channel, GPIO.OUT, initial=GPIO.HIGH)\nGPIO.output(Relay_channel, GPIO.LOW)\n#sleep(1)\nmsg = {\n  \"topic\": \"Heat\",\n  \"payload\": \"ON\"\n}\nreturn msg","outputs":1,"x":520,"y":280,"wires":[["18b1a671.a781da"]]},{"id":"852beccf.7902d","type":"python3-function","z":"394fcece.136712","name":"Heat Off","func":"import RPi.GPIO as GPIO\nfrom time import sleep\nRelay_channel = 17\nGPIO.setwarnings(False)\nGPIO.setmode(GPIO.BCM)\nGPIO.setup(Relay_channel, GPIO.OUT, initial=GPIO.HIGH)\nGPIO.output(Relay_channel, GPIO.HIGH)\n#sleep(1)\nmsg = {\n  \"topic\": \"Heat\",\n  \"payload\": \"OFF\"\n}\nreturn msg","outputs":1,"x":520,"y":200,"wires":[["18b1a671.a781da"]]},{"id":"18b1a671.a781da","type":"ui_text","z":"394fcece.136712","group":"df17b484.5087f8","order":8,"width":2,"height":1,"name":"Heat Status","label":"Heat: ","format":"{{msg.payload}}","layout":"row-left","x":710,"y":240,"wires":[]},{"id":"61ae0fb0.534dc","type":"ui_button","z":"394fcece.136712","name":"","group":"df17b484.5087f8","order":10,"width":2,"height":1,"passthru":false,"label":"Heat Off","tooltip":"","color":"","bgcolor":"","icon":"","payload":"Off","payloadType":"str","topic":"Heat","topicType":"msg","x":340,"y":200,"wires":[["852beccf.7902d"]]},{"id":"380c5c08.574804","type":"ui_button","z":"394fcece.136712","name":"","group":"df17b484.5087f8","order":9,"width":2,"height":1,"passthru":false,"label":"Heat On","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":340,"y":280,"wires":[["a4b8a596.b58208"]]},{"id":"21499269.7335ae","type":"inject","z":"394fcece.136712","name":"1 sec","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":90,"y":120,"wires":[["f12b409.03550c"]]},{"id":"d6f0e66d.b954d8","type":"delay","z":"394fcece.136712","name":"Limit","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"5","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":370,"y":120,"wires":[["ee67fbe.e613308","7a68b6ba.b07c68","fd8eb990.89e448","336a5d64.79aa02"]]},{"id":"54991333.36cc3c","type":"ui_group","name":"Environment (BME280)","tab":"fc7ad6e.40b8028","order":5,"disp":true,"width":12,"collapse":true},{"id":"df17b484.5087f8","type":"ui_group","name":"Gasses (SGP30)","tab":"fc7ad6e.40b8028","order":6,"disp":true,"width":12,"collapse":true},{"id":"fc7ad6e.40b8028","type":"ui_tab","name":"Grow Tent","icon":"dashboard","order":2,"disabled":false,"hidden":false}]

Locked
Please be positive and constructive with your questions and comments.

Return to “For Educators”